WIP support sets (well, kinda of) - #79
Conversation
adamgundry
left a comment
There was a problem hiding this comment.
Thanks @adinapoli, I think sets would be very useful to support more natively in api-tools. I have a few concerns about the CBOR-related details and I think we can do something simpler, but the general idea seems good to me.
Add TySet with Set T syntax. Generated Haskell types use Data.Set.Set, while JSON and CBOR wire formats remain plain arrays: decoding accepts arrays in any order and drops duplicates, encoding uses the element order of the corresponding Set. This avoids bespoke CBOR set tags and reuses the existing list default value for set-valued fields.
8b97056 to
37cab74
Compare
Follow-up to "Support set types in API schemas", addressing gaps found while reviewing that change against Adam's review of #79. * Generated traversals now descend into set-valued fields. Previously traverser' returned Nothing for TySet, so traverser fell back to `const pure` and generated traversals silently skipped sets: values of the traversed type nested inside a `Set T` field were never visited. Data.Set.Set is not Traversable (its element type is constrained by Ord), but we can traverse via the element list, so add traverseSet and use it. Note this may shrink the set if the function maps distinct elements to the same value. * Add "Set" to Data.API.Scan.keywords, as the N.B. in Scan.x requires for any new keyword. * Cover TySet in the migration tests. UpdateSet, expectSetList and the implicit set default were previously untested: - Foo gains `ens :: Set AnEnum` in the start and end schemas, so the 2.1 AnEnum alternative rename migrates through a set, in both the JSON and the generic Value paths. - version 2.6 gains `field added noset :: Set string`, exercising defaultValueForType at TySet. * Add a test that the generated traversal visits set elements. * Document that set element types need Ord, and that records and unions do not derive it by default, so a set of such a type requires datatypesTool' (or the defaultDerivedClasses setting). * Note in the changelog that Set is now a reserved word, so a type previously called Set must be written 'Set'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adam's review of #79 asked for the JSON representation of a set to be simply the list of its elements, order-insensitive when decoding. That is what the FromJSONWithErrs instance now does, but nothing asserted it: add decoding cases covering an out-of-order array with a duplicate element, and an object being rejected (the shape the original branch accepted via jsonParseCborSet). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adamgundry
left a comment
There was a problem hiding this comment.
This is looking pretty good, thanks @adinapoli!
| -- | A set on the wire. We retain the serialised order rather | ||
| -- than sorting in the generic representation, so that generic | ||
| -- CBOR decoding agrees with type-specific decoders. | ||
| | SetList ![Value] |
There was a problem hiding this comment.
Do we need to add SetList to the generic Value representation, or can we get away with List? In particular it would be nice if changing a type between a list and a set didn't need a data migration.
There was a problem hiding this comment.
Ah hold on, I see this is addressed in the PR comments (but not in the code, that I could see):
Valuegains aSetListconstructor which preserves wire order — the decoder never sorts, so anOrdinstance on the concrete type that disagrees with the ordering of the correspondingValues can't cause a mismatch.SetListalso encodes withencodeListLen(definite length), matching serialise'sencodeSetSkel, where lists use the indefinite-length encoding.
SetListis deliberately a separate constructor fromList:Value.encodehas no
schema to hand and dispatches purely on the constructor, so it can't otherwise tell
which length encoding to use.
So the problem here is that serialise encodes lists and sets differently on the wire, so it is nontrivial for us to avoid distinguishing them. Perhaps we will have to keep SetList after all, and accept that migrating between lists/sets changes the CBOR representation.
| module Data.API.Tools.Traversal | ||
| ( traversalTool | ||
| , traversalsTool | ||
| , traverseSet |
There was a problem hiding this comment.
I don't think this is needed in the public API?
| , traverseSet |
| -- | Traverse the elements of a 'Set.Set'. This is not a lawful 'Traversal' | ||
| -- (it cannot be, because 'Set.Set' is not a 'Functor'), but it is what the | ||
| -- traversals generated by 'traversalsTool' use for set-valued fields. |
There was a problem hiding this comment.
Let's add this to the comments on traversalTool explicitly saying that if the schema uses sets, the generated traversal will be non-lawful as it may coalesce multiple elements that are mapped to the same value.
| -- | Represents the positions in a type to apply an update | ||
| data UpdateTypePos | ||
| = UpdateList UpdateTypePos | ||
| | UpdateSet UpdateTypePos |
There was a problem hiding this comment.
Hopefully if we can get rid of SetList then UpdateSet also becomes unnecessary?
| = record | ||
| ints :: Set integer // a set of integers | ||
| flags :: Set Flag // a set of boolean newtypes | ||
| utcs :: ? Set utc // an optional set of UTC timestamps |
There was a problem hiding this comment.
As we discussed, it would be nice to be able to write Maybe (Set UTCTime) for ? Set utc and similarly Set Int instead of Set integer (while keeping backwards compatibility for the old syntax). I suggest we save that for a follow-up PR though.
Support
Set Tin schemasRewritten from scratch on top of
master, addressing @adamgundry's review of #79.Schemas can now say
Set T; generated Haskell usesData.Set.Set; JSON and CBORstay plain arrays.
Review points from #79
jsonParseCborSet.FromJSONWithErrs (Set a)decodes aJS.ArrayviaSet.fromList(order-insensitive, dedups); encoding is aeson'sToJSON (Set a), i.e. ascending.Set Tsyntax, not{T}. Note this makesSeta reserved word — a typepreviously called
Setmust now be written'Set'.DefValSet;DefValListcovers both, anddefaultValueForTypereturnsit for
TySettoo.fromDistinctAscListanywhere. Nothing assumes sorted or distinct input.Data/Binary/Serialise/CBOR/Extra.hsisuntouched, so the licensing question goes away with it.
Valuegainsa
SetListconstructor which preserves wire order — the decoder never sorts,so an
Ordinstance on the concrete type that disagrees with the ordering ofthe corresponding
Values can't cause a mismatch.SetListalso encodes withencodeListLen(definite length), matching serialise'sencodeSetSkel, wherelists use the indefinite-length encoding.
prop_cborGenericcompares FlatTermsexactly, so both of these are load-bearing.
SetListis deliberately a separate constructor fromList:Value.encodehas noschema to hand and dispatches purely on the constructor, so it can't otherwise tell
which length encoding to use.
Also in here
Setisn'tTraversable,but traversing via the element list works; previously
traverser'returnedNothingforTySetand the generated traversal silently left sets alone.Note this can shrink a set if the function is not injective.
Setadded toData.API.Scan.keywords.Setfields in both example schemas, plusFoo.ens :: Set AnEnumin the migration schemas so the 2.1 enum rename migrates through a set
(
UpdateSet,expectSetList), andfield added noset :: Set stringat 2.6 forthe implicit set default. Decoding cases for out-of-order/duplicate arrays and
for objects being rejected.
Known limitation
Set element types need
Ord, and records/unions don't derive it by default, soSet SomeRecordneedsdatatypesTool'to add it. Documented in the tutorial andchangelog. Inferring it from the API (deriving
Ordfor types used as setelements) would be nicer but changes the
defaultDerivedClasses :: APINode -> [Name]signature.