Skip to content
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

Merged
merged 12 commits into from
Jun 29, 2021

Conversation

KtorZ
Copy link
Collaborator

@KtorZ KtorZ commented Jun 24, 2021

  • 📍 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 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...

  • 📍 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'

@KtorZ KtorZ requested a review from a team June 24, 2021 16:27
@KtorZ KtorZ self-assigned this Jun 24, 2021
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] }|]
Copy link
Member

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?

Copy link
Collaborator Author

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 ?

Copy link
Collaborator Author

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 😶)

Copy link
Member

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:

Suggested change
sendRequest n3 [aesonQQ|{"req": "commit", "args": [3] }|]
sendRequest n3 $ object [ "req" .= "commit", "args" .= [3] ]

Copy link

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.

Copy link

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.

Copy link
Collaborator Author

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)
Copy link
Member

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?

Copy link
Collaborator Author

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 ^.^

clientRequestJsonOptions :: Aeson.Options
clientRequestJsonOptions =
Aeson.defaultOptions
{ constructorTagModifier = camelCase
Copy link
Member

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

Copy link
Member

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

Copy link

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.

Copy link
Collaborator Author

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.

Copy link
Member

@ch1bo ch1bo left a 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.

Copy link

@ghost ghost left a 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.

deriving (Eq, Show)

withAPIServer ::
Tx tx =>
(Tx tx, FromJSON tx, FromJSON (UTxO tx)) =>
Copy link

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 ?

Copy link
Collaborator Author

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 👍

clientRequestJsonOptions :: Aeson.Options
clientRequestJsonOptions =
Aeson.defaultOptions
{ constructorTagModifier = camelCase
Copy link

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.

{ tagFieldName = "req"
, contentsFieldName = "args"
}
}
Copy link

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

Copy link
Collaborator Author

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] }|]
Copy link

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] }|]
Copy link

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.

Copy link

@ghost ghost left a 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.

@KtorZ KtorZ force-pushed the KtorZ/JSON-for-interface branch from 65ec25d to 4b5864d Compare June 28, 2021 10:04

-- 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'
Copy link
Member

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?

Copy link
Collaborator Author

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.

@KtorZ KtorZ force-pushed the KtorZ/JSON-for-interface branch from d42fd29 to a769cc2 Compare June 28, 2021 16:46
KtorZ added 12 commits June 28, 2021 18:48
  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...
@KtorZ KtorZ force-pushed the KtorZ/JSON-for-interface branch from a769cc2 to f957ff1 Compare June 28, 2021 17:02
@ch1bo ch1bo merged commit 72aeeb0 into master Jun 29, 2021
@ch1bo ch1bo deleted the KtorZ/JSON-for-interface branch June 29, 2021 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants