structio 0.5.0
Ordered objects, JSON carried through as its text, a write-only declaration, and Value comparisons that cover the whole table. Two breaking changes, both narrow: Value::Object is an OrderedMap<Value>, and the new comparison impls can make a bare .into() ambiguous.
[dependencies]
structio = "0.5"
# The derive is off by default:
structio = { version = "0.5", features = ["derive"] }Changed
-
Value::Objectkeeps its member order. It is anOrderedMap<Value>rather than aBTreeMap<String, Value>, so a document read into aValueand written back out lists its members in the order it arrived in, andto_valueyields a declared type's field order. Equality is unchanged in meaning and ignores order, so two values can compare equal and write different text.sort_keys()on the object gives back the sorted output. Breaking for code namingBTreeMapwherestructio::Objectis expected, or relying on sorted output. -
transparentis stage 2 of the derive, not stage 3. It describes the whole type rather than one field: stage 2 is for a shape the macros cannot declare, stage 3 for per-field policy. It is not implemented and the derive still refuses it, now naming stage 2.
Added
-
A
Valuecompares with the primitive it holds.doc["port"] == 8080and8080 == doc.get("port").unwrap()are comparisons now rather than avalue!(8080)to wrap the right-hand side in: every integer width,f32,f64,bool,str,&str,Stringand&String, on either side, with the value owned or behind either reference the accessors hand back. Onlystr,&strandboolhad an impl before, and only against an owned value, so most of the table was a compile error and which part was a matter of luck. A number is met at the comparand's width:value!(1) == 1.0andvalue!(1.0) != 1, being the different numbers this crate keeps them, and against anf32the stored number rounds to that width, so a document's0.1equals0.1f32while one too large or too small to round to anf32equals none. Breaking for a caller whose right-hand type was pinned by there being onePartialEqimpl within reach: withn: u64,n == w.into()is now ambiguous betweenu64andValueand needs the type named. -
Error::key_in, the name an unknown key had.UnknownKeyandUnknownVariantcarry no name, a name the document chose being no&'static str, so the error winds back to it and reports the offset. This reads it back out of the document at that offset, unescaping as the reader would, and hands aMissingKeythe static name it already carries. The lifetime is the document argument's, not the error's, soErrorstaysCopyand independent of the buffer. JSON only: a BEVE key's length lives in a prefix the offset is already past. -
read_map_located, onjson::Parserandbeve::Reader.read_mapcalls back after the colon, so a hand-written map reader could see a key's name but not its position, and hand-rolling the loop was no way out either, the depth counter that bounds nesting not being public. This reports the offset alongside the key: the same byte a generated reader'sUnknownKeynames, so an error raised by hand reads like one the crate raised. -
structio::OrderedMap<V>, a string-keyed map that keeps its insertion order. Entries live in one vector in the order they arrived; below nine keys a lookup scans it, above that a robin hood hash table indexes it. It reads and writes in both formats, so it works as a whole document or as a declared field whereBTreeMapwould, andObjectisOrderedMap<Value>. Equality ignores order;sort_keys()reorders by key. -
write_only, a declaration of the write half alone.object!(write_only ..), and the same token in front ofarray!,unit_enum!,tagged_enum!and the one-format macros, generate the writing impls and no read at all, so a field's type needs noReadimpl and noDefault.#[structio(write_only)]is the derive's spelling of it, and the bytes written are unchanged either way. A generic one bounds its type parameters by the newstructio::Writerather than bystructio::ReadWriteandDefault.#[required]is refused, being a rule about reading, and there is noread_only. docs/schemas.md has the rest. -
A failed
ReadorWritebound now explains the direction axis.Read,Write,ReadAsandWriteAs, in both formats, carry#[diagnostic::on_unimplemented]notes naming what discharges the bound:write_onlywhere the struct is only ever written, askip_value()stub where one field is, and on the write sidewrite_null()withis_nullreturningtrue, since a member that writes nothing truncates the object. -
json::Raw, one JSON value carried through as its text. A field that captures the exact bytes of a value on read and emits them unchanged on write, so a forwarded body keeps its key order, its number spellings and its escapes: whatValueis not, being a tree that respells its numbers and decodes its escapes. Reading borrows the span out of the document, and underALLOW_COMMENTSstrips the comments out of a span that carries any, owning that one; writing is one copy of those bytes, laid out again at the right depth underPRETTY. JSON only, so a struct with one is declared withjson_object!. docs/schemas.md has the rest. -
json::prettify_value_into. Lays one JSON value out into aWriterthat is already part-way through a document, at that writer's current depth and under its policy.json::Rawwrites through it underPRETTY, and it is what a passthrough type of your own needs so that a forwarded value is indented against its neighbours rather than emitted as a blob. -
Tuple structs, in
array!and in the derive. A positional declaration names a field by its position,array!(Entry [0, 1]), so the shape that has no field names is now the shape it takes most naturally:#[derive(Structio)]with#[structio(array)]accepts a tuple struct, with the order,.., an element type and generics all as they are for a named struct. The bytes are the tuple's, as they always were. Declared as an object it is still refused, an object having nothing for its keys to be, and the message now names#[structio(array)]. -
Writer::member_key, for an object key known only at run time.membertakes the key already prepared, quoted with its colon in JSON and length-prefixed in BEVE, because that is what a declaration assembles at compile time. A hand-writtenWriteObjectwhose keys come off a walk had to build those bytes itself, and it went wrong differently in each format: nothing escapes a JSON key on that path, so a key holding a"or a\wrote a document no reader takes, and a BEVE member laid down withsizeandrawis not counted, which a debug build catches against the header the object already committed to. This takes the key itself, escapes or length-prefixes it, counts the member, and honoursSkipNullexactly asmemberdoes;member_key_withis the adapter form. The policy's boundary is a struct's member against a map's entry, not a compile-time key against a computed one, andwrite_keyedis still the map. docs/schemas.md has the rest. -
json::Parser::rest_str. The&strcounterpart ofrest, for a hand-writtenReadimpl capturing a span: the input's UTF-8 validity is already known, so nothing has to establish it a second time.
Fixed
-
A
Matrixnames the unknown key it refused. It reads its three members by hand through a map callback, which runs after the colon, so itsUnknownKeyreported the offending member's value rather than its key: a caret under the value, andkey_inreading a name off it. It winds back to the key, so everyUnknownKeyin the crate now names a key. The reported offset for this one code on this one type moves. -
The whole-key hash now folds in the key length. It read whole 8-byte chunks and then an overlapping tail, so two keys differing only in the bytes between them, such as
field_name_10500_valueandfield_name_105000_value, hashed the same under every seed and cost the object its hash: it read under a linear scan instead. Keys shorter than 8 bytes were zero filled, so trailing NULs vanished the same way. Never a wrong field, since a candidate is always confirmed by a full comparison, only a slower read. -
A declared type does not need
Default. docs/derive.md said it did, flatly, contradicting docs/schemas.md.Defaultis required where a read constructs a value: the entry points that return one, anOption's payload, a growingVec's tail, a map's values, an enum variant's payload. A type that is only ever written needs none, and the derive's examples no longer imply otherwise. -
Where an error out of a declaration lands. The same file promised that a field whose type has no
Readimpl is reported at that field. One macro call covers every field, so it is reported at the declaration: the struct's name under the derive, the whole invocation under a hand-written one. What does land where it was written is the derive's own refusals and an adapter named bywith = "..". -
The same rule, in the README. It read as though reaching for
read_intolifted theDefaultrequirement. It lifts it for the value handed in and for nothing beneath it, soread_into(&mut Vec<T>, ..)still asksTfor one and no spelling of the read avoids it;Box<T>and[T; N]do escape it, having no element to build. And a tagged enum's payloads needDefaultwhere the enum itself does not, which the types table had as both.tagged_enum!now shows the all-payload shape that cannot derive one.
Upgrading
Value::Object is an OrderedMap<Value>. Code that named BTreeMap<String, Value> where structio::Object is expected names structio::Object or OrderedMap<Value> instead. get, insert, remove, len, iter and indexing by key are all still there, so most code that goes through the accessors is unchanged. What does change is output: an object now writes its members in the order they arrived rather than sorted. sort_keys() on the object gives the old ordering back, and equality still ignores order, so a comparison that passed before passes now.
A Value comparison may need its type named. There was one PartialEq impl within reach for most right-hand types, so inference had one candidate to pick; there is now one per width. With n: u64, n == w.into() is ambiguous and wants n == u64::from(w) or a named type. An explicit comparison such as doc["port"] == 8080 is what the new impls are for and needs nothing.
Nothing else in a 0.4.0 declaration changes its bytes, and nothing about the macros' or the derive's accepted syntax changes.