Repository navigation
Releases: hyphatech/wiretype
Release list
0.6.0
Schema.Melangeprints the components as one OCaml module for Melange, from the same walk as JSON Schema and Zod: a type per component and the functions that read and write it, overJs.Json,Js.DateandJs.Dictalone, each listing every problem at its place with the server's codes and sentences. A number is afloat; an integer anintwhere its bounds lie within 32 bits and otherwise a wholefloat, refused past 2^53 asz.int()refuses it; an enum and a union by tag are variants; a list anarray; an instant, a duration and a UUID the module's own abstract types. The browser is the looser in four places, by design -- a whole number past 2^53, a member given twice, nesting, and a URI, held to the browser'sURL-- and the kinds' rows and a file of shared rows hold it to the server's verdict everywhere else. Melange is a test dependency of wiretype alone: a native user installs nothing new.- The Zod rows run on Bun, where they ran on Node.
Object.memandObject.opt_memtake?field, the OCaml field a member is in, and the deriver gives it wherever a field's name is not the member's:Schema.Melangenames the browser's field so,kindfor a member"type"that the server callskind, where it madetype_. A schema'spropholds it, so apropbuilt by hand gainsfield = None, which is breaking.- Breaking: what a schema says of a number is
Schema.Number.t, and of a stringSchema.Text.t, each withanyfor one with nothing said of it:String { Schema.Text.any with max_length = Some 8 }. They wereSchema.numberandno_bounds,Schema.string_andtext.
0.5.0
Value.write's documentation says it checks nothingencodechecks -- a non-finite number is writtennull, text that is not UTF-8 and a member given twice as they are -- where it said it wrote asencodedoes.- Breaking: one spelling for a description over a type of one's own, as in spindle and rowtype: the function from what the description reads is
~parseand the one back is~print.map ?dec ?encismap ?parse ?print, andShape.map's fieldsdecandencareparseandprint. A description's name is~kindeverywhere, sokind ~nameiskind ~kind. - Breaking:
uuidis aUuidm.t, where it was astring, as a UUID is in rowtype and postgres-eio, so one identifier crosses them as one type. It is read in either case and written in lower case, and one of another version than asked for, or of no variant RFC 9562 defines, isunspellableon writing. The deriver describes aUuidm.tby it. Thewiretypepackage depends on Uuidm (0.9.10 or later), which depends on nothing. - Breaking:
encodechecks every bound its description states -- a least and a most, a multiple, a length, a count of items or of a map's members -- and refuses a value outside one with the newUnwritable.Out_of_bounds, at its place, in the sentencedecodesays it in, where it wrote a documentdecodethen refused. An example a schema publishes is written byencode, so one outside its member's bounds is a schema'sUnwritableerror. A bounded value costs a few per cent more to write, and allocates nothing more: a record of four bounded members, ten thousand of them, is written in 7.5 ms where it took 7.0. - Breaking: a member's access is kept on the wire as well as in the schema.
encodeleaves out a write-only member, where it wrote a password into every answer, anddecodenever reads a read-only one: one a request holds is skipped, as a client sends back the object it was given, and the member reads as itsabsent, where a client could set the server'sidand a request as its schema describes it was refused for leavingidout.Object.finishraises for a read-only member with noabsentin an object that is read, which it could not build, and the deriver refuses a[@read_only]field with no[@default]that is no option, saying to give it one. - Breaking:
Shape.accessandShape.uuid_versionare ordinary variants, where they were polymorphic ones, as a closed set is in every Hypha library:~access:Read_onlyand~version:V7, the type naming the constructor, where~access:`Read_onlywas written. - Breaking: the deriver reads a member as deprecated only by
[@wiretype.deprecated], where OCaml's own[@deprecated]was taken for it: one with a payload,[@deprecated "use y"], was refused, and a bare one made the derived code use the field it marks, which the compiler's alert then stopped. A field or a constructor OCaml's attribute marks is derived as any other, and its alert is left for the code a person wrote. - A description of itself is built by the domain that first reaches it, and another reaching it then waits for it, where it was told
Lazy.Undefinedanddecodeandencoderaised it. A recursive type with parameters,'a tree, is built a level at a time as it is first read, so four domains reading one at once raised it at every level none had reached yet; the 0.4.0 fix built only those with none. A hand-writtenrec'need no longer be forced before it is shared.
0.4.0
- A union inside a union's member is read in time that grows with the document alone, where each union went through everything inside it again to find its tag: a recursive variant 500 deep around a megabyte of text took a second to read, and takes what one level does.
- The deriver builds a recursive type's description as its module loads, where it was built on first use, and two domains reading it first at once raised
Lazy.Undefined.rec'says to force a hand-written one before sharing it between domains. - A map whose key is read in more than one spelling -- a
uuidin either case, aninstantat any offset -- refuses a name that spells a key already given asrepeated_member, as the map's odoc says, where it read both, andencodethen refused the map it had read. - Breaking: a component's schema is walked from its own root wherever it is reached, so kinds that hold each other in a ring of three or more refer to each other, where the third's member back to the first was printed as any JSON (
a: z.optional(z.unknown()), nowget a() { return z.optional(ASchema); }) and reported loose. A request's<Name>Inputis decided the same way, so a ring whose difference is more than three kinds away is namedInputfrom wherever it is walked. A recursive description with no kind is expanded to the same depth inside every component, and past a hundred kinds each inside the last -- reached only by one that makes a new kind at every level -- the walk prints any JSON and reports it. - Breaking: the deriver gives a type with parameters no kind unless
[@@kind]names it, where it was named by the type whatever its arguments, sotype 'a pageused asstring pageandint pagein one document waskind_sharedand both printed as onePageSchema. Each use is now written where it is (users: z.object({ items: z.array(z.string()), ... })), andWiretype.nameof one isobject. A recursive type with parameters is expanded as far as any recursion with no kind, unless[@@kind]names it.
0.3.0
-
Breaking:
instantis aPtime.t tanddurationanMtime.Span.t t, where each was anint tof milliseconds, anintthat said neither its unit nor that it was a time. What each reads and writes, and the JSON Schema and Zod each prints, are as they were. An instant is written to the millisecond, a fraction past it dropped toward the past, and can no longer fail to be written, since aPtime.tis of the years 0000 to 9999. A duration is written to the millisecond, and is never negative. -
Breaking: a duration longer than an
Mtime.Span.tholds, 2^64 - 1 nanoseconds or about 584 years, is refused asmalformed, where one up to a hundred thousand years was read. -
The
wiretypepackage depends onptimeandmtime, where it depended on nothing; both depend on nothing themselves. -
The deriver describes a
Ptime.tasWiretype.instant, and anMtime.Span.tor anMtime.spanasWiretype.duration, where the derived code failed to compile on an unboundPtime.json, and refuses a case whose one argument is one, as it refuses a scalar. -
Breaking:
dictlabels its two descriptions and ends in():J.dict ~key:J.string ~value:(J.uuid ()) (), whereJ.dict J.string (J.uuid ())andJ.dict (J.uuid ()) J.stringboth compiled as a(string * string) list t, the one reading a uuid's values and the other its names. The()is what lets?min_propertiesand?max_propertiesbe left out. The deriver writes the new call. -
A description that comes back to itself before it reads anything --
let rec t = lazy (J.rec' t), or through amapor anany-- is refused at its place, bydecodeasmalformedand byencodeasunspellable, where reading or writing went round for ever or ended inStack_overflow. -
Breaking: the deriver reads a type written by its stdlib module's name --
String.t,'a List.t,Stdlib.Int64.t-- as its short name, bounds and an option's member alike, where the derived code failed to compile on an unboundString.json;Stdlib.Int32.tand its kin are refused asint32is. A module of the user's own namedStringis read as the stdlib's: describe its type with[@with String.json]. -
Breaking: a value
encoderefuses astoo_deepis at no more than the first 16 segments of its path, its sentence saying how many levels below ([0][0]...: This is nested more than 512 deep, 496 levels below here.), asdecode's problem is, where its path went the whole way down, 512 segments and more. -
The deriver refuses an
int32, anativeint, acharandbytes, orInt32.t,Nativeint.t,Char.tandBytes.t, where each is written, saying what to write instead, where the derived code failed to compile on an unboundint32_jsonorInt32.json. -
Breaking:
decodelists at most a hundred problems, or as many as a new?max_problemssays; past it, reading stops and the list ends with one problem of a new code,too_many_problems(Problem.Too_many_problems, which a match onProblem.codewants a case for), at the document. A body of a megabyte of wrong items was a quarter of a million problems. -
Breaking: a
too_deepproblem is at no more than the first 16 segments of its path, its sentence saying how many levels below ([0][0]...: This is nested more than 512 deep, 496 levels below here.), where its path went the whole way down: 1.5 KB at the default limit, and 30 KB at 10,000. -
Below 0,
decode ~max_depthis 0, as one past 10,000 is 10,000: a scalar alone is read, and the problem says it is nested more than 0 deep, where it said more than a negative number. -
Breaking: a path writes a member's name quoted as JSON where it holds a C1 control (NEL among them), U+2028, U+2029 or a bidirectional control (U+061C, U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069), each as JSON's
\uescape (items["a\u2028b"]), where it wrote them as they were: a problem printed for a log could end its line there, or show its text in another order than it has. -
Breaking: the doc of an enum, a kind, a map, a value of several sorts (
any) and a union is in the schema, as an object's and a member's were: JSON Schema'sdescriptionon its subschema, and Zod's comment on the declaration or member it stands for. A derived enum's or union's doc comment is so too.Schema.thas a new constructor,Described, which wraps a shape with its doc; a match onSchema.twants a case for it. A member's doc wins over its shape's on its property, which held twodescriptions where the member's shape was an object with a doc and no kind. The ready-made kinds print no doc of their own, since theirformatsays what they are. -
Breaking: OCaml 5.5 or later is required. The opam files asked for 5.4, which CI tested and nothing else did; every switch the code is written in is 5.5.
-
Breaking: an
intorint64written with a fraction or an exponent is whole where its digits are, never where the double it rounds to is:1.0000000000000000001and1.5e-400are fractions, where they were read as1and0. One of 2^53 or more so written istoo_largeortoo_small, since a double cannot say which whole number it is:9007199254740993.0was read as9007199254740992. -
Breaking:
encoderefuses, asunspellableat its path, aValue.Numberthat is not finite in any JSON, which it wrotenullthough its documentation said such a float is not written.Value.writestill writes itnull, and says so. -
Breaking:
Object.opt_memover anullableraisesInvalid_argument, and the deriver refuses anoption optionfield, sinceSome Nonewas writtennulland read back asNone. A member that tells a missing value fromnullismem ~absentover anullable:[@default e]on anoptionfield. -
Breaking:
nullableandObject.opt_memraiseInvalid_argumentover any description that writesnullfor a value of its own -- anullablewherever it is (a list's item, a map's value, a member with~absent),null,Value.json, ananythat readsnull, or amaporkindover one -- sinceNoneandSomeof that value were both writtennull, and read back asNone. A description of itself still being made cannot be looked into, soencoderefuses, asunspellable, aSomeit would write asnull. The deriver refuses an option of an option, or ofunit, wherever it is written, a field with[@default e]among them. A member that may hold any JSON ismem ~absent:Value.NulloverValue.json. -
Breaking: a bound no value meets raises
Invalid_argumentwhere the description is built -- a least past its most inint_bounded,int64_bounded,number_bounded(~above:1. ~max:1.among them),string_bounded,listanddict, or a negative length or count -- where it was taken and every document then refused. The deriver refuses such a pair written as literals ([@min 5] [@max 3]). -
Breaking: a derived variant whose one argument is no object --
A of int, a list, an array, an option, a tuple -- is refused where it is written, saying to give it a name in an inline record, where it compiled and raised when the program started.Object.Case.map, refusing one, says what the description is and to describe an object.
0.2.0
- Breaking: text that is not JSON, or nested past the limit, is refused with that one problem alone. A plain object or list also reported the problems it had found before it and a union never did; now every description answers as a parse before the reading would. A value that is not JSON is no longer also said to be the wrong sort, guessed from its first byte:
nopeas "not null". - A member a union refuses is said before any problem inside it, in the document's order, as a plain object's always was.
- Breaking: an int's or an int64's
multiple_ofis printed to zod as an exactz.refine, since zod'smultipleOftakes3000000000000001for a multiple of 3 and the reader, rightly, does not. - Breaking: an
intorint64past a double, as1e400, istoo_largeortoo_small, where it was said to be a fraction (unexpected_type). - Breaking: a path writes a member's name quoted as JSON where it is empty or holds anything but letters, digits,
_,-,$and non-ASCII text (items["a.b"]), so a problem is one line for a log whatever a document names its members, and two paths are never spelt alike. - Breaking:
encoderefuses, asunspellable, a float that is not finite, which it wrotenullthoughnumberreads nonull; a value an enum has no word for; and a case its union does not list.Object.finishraises where an object that is read has a member or a tag withomitand noabsent, which it wrote by leaving out and read as required. - Breaking: the schemas say what the reader means or report that they cannot. A value of several sorts that nothing reads is reported loose, where it was printed as
null; an integer's bound past 2^53 is left out and reported loose, where it was rounded; a UUID of one version has itspatternin JSON Schema; a member named__proto__is a computed key in zod, where it set the prototype. - Breaking:
number_boundedraises for a bound no number meets --~max:neg_infinity, or NaN -- and takes one every number meets,~max:infinity, as none; each was printed asnull. - Breaking: two descriptions under one kind that differ only in a doc, an example or
deprecatedare akind_sharederror, where the second was dropped. - A schema walks each component once, where a chain of kinds each used twice took twice as long for every link.
- A union's case may be an object reached through
rec', as a derived union whose case is a record of its own group is:Object.Case.maprefused it as no object when the program started. ppx_wiretypederives a recursive group that mixes a type with parameters and one without, a parameter beside a type its argument was named after, and a type where the caller has a|>of their own: each derived code that did not compile, or used the wrong description.- Breaking:
ppx_wiretyperefuses an attribute spelt[@wiretype.x]for no attribute it has, one of its attributes where it does not read it, and[@@tag]on a type that is no union, each of which it ignored; and[@above]or[@below]on anintwith what to write instead. Its refusals say what to write, and two constructors written alike are refused at the second.[@above]and[@below]are in its.mli. - A union's object is read in time linear in its members; each was said again with every problem found before it, so 40,000 members took 23 seconds.
urirefuses anxn--label longer than DNS's 63 octets before decoding it, since decoding one takes time growing with the square of its length.- Breaking:
decode'smax_depthis taken to 10,000 at most, past which the reader's own recursion could overflow the stack; andnamenames a description of itself, where it overflowed the stack. - Breaking:
ipv6refuses a dotted IPv4 address anywhere but at the end, as in1.2.3.4::, andurirefuses afile:URI with userinfo, a port or a host that is no domain or address, and an empty host after userinfo or before a port: each a string the URL standard, and so zod, refuses.
0.1.0
First release.
wiretype: a JSON shape described once -- scalars with their bounds, enums, lists, tuples, maps, nullables, objects, unions on a tag, values of several JSON sorts, kinds over another description, any JSON, and descriptions of themselves.- Reading in one pass, with every problem found at its path as a code and a sentence; a member given twice, anywhere in a document, is refused.
- Writing minified, with each double in its fewest digits, refusing what reading would: text that is not UTF-8, a member given twice, nesting past 512.
- JSON Schema 2020-12 and zod/mini printed from one walk, a request's component named
<Name>Inputexactly where it differs by direction, and what cannot be said exactly reported as loose. - Ready-made kinds for instants, dates, durations, UUIDs of RFC 9562's versions, base64, URIs and IP addresses, each accepting no string its zod check refuses, and each bound decided as zod decides it.
ppx_wiretype:[@@deriving wiretype]for records, enums, tagged unions, polymorphic variants, type parameters and recursive types, with bounds, defaults, read-only and write-only members, docs and examples as attributes, writing only the public combinators and refusing at compile time, at its place, every type it cannot describe.- A description that can mean nothing raises
Invalid_argumentwhere it is built, when the program starts; nothing else raises.