-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use structured JSON for interfacing with 'ClientRequest' / 'ClientResponse' #29
Conversation
sendRequest n3 "Commit (fromList [3])" | ||
sendRequest n1 [aesonQQ|{"req": "commit", "args": [1] }|] | ||
sendRequest n2 [aesonQQ|{"req": "commit", "args": [2] }|] | ||
sendRequest n3 [aesonQQ|{"req": "commit", "args": [3] }|] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need quasi quotes here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it's handy and readable for constructing JSON ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(if you have a better proposal, I am glad to hear it 😶)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have used "builders" for an Aeson.Value
in the past, but have also seem full string quasi-quotation. This seems to be something in between from the looks of it. Personally I prefer to use the former as the compiler helps in finding bugs when creating the json AST.
The following should be equal, but I would be interested in seeing how the two approaches compare in a more complicated example:
sendRequest n3 [aesonQQ|{"req": "commit", "args": [3] }|] | |
sendRequest n3 $ object [ "req" .= "commit", "args" .= [3] ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also favor builders, I don't see what QQuoters add here, it adds a dependency and compillation time for little value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And as the name implies, Aeson builders will lead us into test builders which will simplify the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see what QQuoters add here, it adds a dependency and compillation time for little value.
Well, actually, the quoter comes from the aeson library itself so it's not like it is adding much dependency. Plus, it's not a mere string QQ, it does parse the JSON underneath and provides compile-time errors for invalid JSON. The added value to me really being in the "what you see is what you get" kind of spirit. With builders and implicit conversions it's sometimes easy to miss something.
@@ -27,6 +35,12 @@ data SimpleTx = SimpleTx | |||
} | |||
deriving stock (Eq, Ord, Generic, Read, Show) | |||
|
|||
instance ToJSON SimpleTx where | |||
toJSON = genericToJSON (aesonPrefix camelCase) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is creating a JSON object with fields id
, inputs
and outputs
? IMO this "drop all lowercase letters until first uppercase" convention of this aesonPrefix
is only sligthly better than drop 2
and would like to avoid such things as much as possible. Can we stick to just use field names as is and make then non-overlapping or non-conflicting on a case by case basis?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we stick to just use field names as is and make then non-overlapping or non-conflicting on a case by case basis?
I am fine with that ^.^
hydra-node/src/Hydra/HeadLogic.hs
Outdated
clientRequestJsonOptions :: Aeson.Options | ||
clientRequestJsonOptions = | ||
Aeson.defaultOptions | ||
{ constructorTagModifier = camelCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This camelCase
function is basically mapHead toLower
and I know now why you were suggesting to avoid UTxO
as a name as it would turn out as uTxO
using this constructor modifier 😂
Not sure though whether we want to have another dependency just for this very simple modifier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of adding dependencies and doing fancy meta-programming I'd be even open to hand-written instances. Sometimes they are easier to read and maintain than genericXXX
+ various options
and modifiers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am with @ch1bo on this: Let's use Generic
until we hit a snag and then let's write manual instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure though whether we want to have another dependency just for this very simple modifier
I initially wrote them by hand, and ended up writing the same one in two places. Wanted to move in a separate module but it felt a bit too excessive / inappropriate. In the end, aeson-casing is a tiny library with almost no dependency (base and aeson) which kinda does just is needed.
Writing JSON by hand is always doable, just a bit annoying. I think my favorite approach remains generics with default options, and possibly DuplicateRecordFields
when really necessary. With NamedFieldPuns
it's usually not a big problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The aesonPrefix
kinda rubs me the wrong way (have been bitten by drop 16
as a field modifier in the past) and would be interested in alternatives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like @ch1bo I would suggest using "standard" Generic
deriving wherever we can without "fancy" names and JSON wrangling, and use Aeson builders for constructing Value
instances.
hydra-node/src/Hydra/API/Server.hs
Outdated
deriving (Eq, Show) | ||
|
||
withAPIServer :: | ||
Tx tx => | ||
(Tx tx, FromJSON tx, FromJSON (UTxO tx)) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have already added Show
and Eq
instances directly in the declaration of class Tx tx
, would it not make sense to do the same for FromJSON
and ToJSON
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought of it, and even considered maybe to pass codecs as a record object actually, to not necessarily tied to JSON all-the-way. Problem is that we'll likely have to have JSON instances anyway so.. the benefits of abstracting away the codec is maybe not that great.
Moving it to Tx
is a possibility 👍
hydra-node/src/Hydra/HeadLogic.hs
Outdated
clientRequestJsonOptions :: Aeson.Options | ||
clientRequestJsonOptions = | ||
Aeson.defaultOptions | ||
{ constructorTagModifier = camelCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am with @ch1bo on this: Let's use Generic
until we hit a snag and then let's write manual instances.
hydra-node/src/Hydra/HeadLogic.hs
Outdated
{ tagFieldName = "req" | ||
, contentsFieldName = "args" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something related to JSON-RPC? Doesn't seem so: https://www.jsonrpc.org/specification
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. But the default names from aeson are tag
and content
which I very much dislike. So I went for something a bit more appropriate IMO.
sendRequest n3 "Commit (fromList [3])" | ||
sendRequest n1 [aesonQQ|{"req": "commit", "args": [1] }|] | ||
sendRequest n2 [aesonQQ|{"req": "commit", "args": [2] }|] | ||
sendRequest n3 [aesonQQ|{"req": "commit", "args": [3] }|] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also favor builders, I don't see what QQuoters add here, it adds a dependency and compillation time for little value.
sendRequest n3 "Commit (fromList [3])" | ||
sendRequest n1 [aesonQQ|{"req": "commit", "args": [1] }|] | ||
sendRequest n2 [aesonQQ|{"req": "commit", "args": [2] }|] | ||
sendRequest n3 [aesonQQ|{"req": "commit", "args": [3] }|] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And as the name implies, Aeson builders will lead us into test builders which will simplify the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding golden master tests using https://hackage.haskell.org/package/hspec-golden-aeson and basic "isomorphism" properties using https://hackage.haskell.org/package/quickcheck-classes-0.6.5.0/docs/Test-QuickCheck-Classes.html#jsonLaws would be a great addition once we start investing in JSON.
65ec25d
to
4b5864d
Compare
|
||
-- NOTE: Somehow, can't use 'genericShrink' here as GHC is complaining about | ||
-- Overlapping instances with 'UTxO tx' even though for a fixed `tx`, there | ||
-- should be only one 'UTxO tx' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we're missing a functional dependency or so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought so. But really, it shouldn't be necessary here. I didn't give it much thought.
d42fd29
to
a769cc2
Compare
Instead, favor hand-written JSON instances over generic with options. The rationale being that generic deriving with options and other meta-programming are often hard to read and easy to shoot oneself in the foot. Instead, using JSON builder makes for transparent definitions at the cost of a small maintainance burden when changing data-types.
QuasiQuoters don't make unanimity, but builders do despite being perhaps a bit _too polymorphic_.
This was slightly more annoying than I initially thought because of the shrinkers. I thought I could get away with `genericShrink` and a `Generic` instance on `ClientRequest`, but this requires the subtypes to have `Arbitrary` instances, which is pretty annoying since we're trying to precisely avoid those. Ended up writing the shrinker by hand, not sure if it's worth it given that the model is small enough in practice anyway...
All used pervasively across the codebase. So good candidates to be factored out in the prelude.
After discussion, we concluded that it is pretty useful to have generic Arbitrary instances readily available for base data-types, and only provide hand-written generators on a case-by-case. This leverages the generic-random package for generating arbitrary instances, and quickcheck for shrinkers when possible.
Enhance readability, as a big unformatted show is like looking for a needle in a haystack. Not good for quick debugging.
The 'real' implementation should likely be more like it was before this commit, a base16 or bech32 encoding of the key. But with the mock cryptos, keys are basically just numbers and it's easier to read than long bytestring in end-to-end tests. Arguably, the end-to-end interface should not change when we change the crypto, so this may be only midly correct...
a769cc2
to
f957ff1
Compare
📍 Use structured JSON for interfacing with 'ClientRequest'
📍 Remove use of aeson-casing
Instead, favor hand-written JSON instances over generic with options.
The rationale being that generic deriving with options and other
meta-programming are often hard to read and easy to shoot oneself in
the foot.
Instead, using JSON builder makes for transparent definitions at the
cost of a small maintainance burden when changing data-types.
📍 Replace use of aesonQQ with builders
QuasiQuoters don't make unanimity, but builders do despite being
perhaps a bit too polymorphic.
📍 Write JSON roundtrip properties for
ClientRequest
This was slightly more annoying than I initially thought because of
the shrinkers. I thought I could get away with
genericShrink
and aGeneric
instance onClientRequest
, but this requires the subtypesto have
Arbitrary
instances, which is pretty annoying since we'retrying to precisely avoid those.
Ended up writing the shrinker by hand, not sure if it's worth it given
that the model is small enough in practice anyway...
📍 Write JSON instances for 'ClientResponse'
📍 Add {From,To}JSON / {From,To}CBOR / Arbitrary instances to hydra-prelude
All used pervasively across the codebase. So good candidates to be factored out in the prelude.
📍 Define Arbitrary instances for base data-types in lib code
After discussion, we concluded that it is pretty useful to have
generic Arbitrary instances readily available for base data-types, and
only provide hand-written generators on a case-by-case. This leverages
the generic-random package for generating arbitrary instances, and
quickcheck for shrinkers when possible.
📍 Refactor End-to-End to use smart-constructors for building JSON requests.
📍 Reply with structured JSON instead of mere 'show'
📍 Rework 'waitForResponse' expectation failure message
Enhance readability, as a big unformatted show is like looking for a needle in a haystack. Not good for quick debugging.
📍 Represent 'Party' as JSON integers
The 'real' implementation should likely be more like it was before
this commit, a base16 or bech32 encoding of the key. But with the mock
cryptos, keys are basically just numbers and it's easier to read than
long bytestring in end-to-end tests. Arguably, the end-to-end
interface should not change when we change the crypto, so this may be
only midly correct...
📍 Rename 'ClientRequest' -> 'ClientInput', 'ClientResponse' -> 'ServerOutput'