Repository navigation
Entity Runtime 0.30.0
Added
- A
service/2orservice/3operation outcome may declareset_if_present, as a creation outcome already could. When the named optional argument leaf is present, the field takes its value, inserted or replaced; when it is absent, the field is left as it was, present with its old value or absent. The schema check, invariants, events, response andchangedread the result, and replay reproduces it. The destination keeps the creation rule — a declared optional field with no default and the argument leaf's exact definition — so a required field is refused as the destination on an operation too. No parent on the operation map's argument path may declare a default, since normalization would then make the leaf present for a caller who sent none; such a definition is refused asconditional_argument_invalid.kernel/1andservice/1still refuse the key. - A
service/3outcome naming one field in bothfulfillsandset_if_presentis refused at registration asDefinitionError::FulfillmentConditionalSetConflict(fulfillment_conditional_set_conflict). - An operation's
set, and an operation outcome'sset, may write{increment: n}under every semantics: the field becomes its value before the operation plusn, a number or a template that resolves to one, and a negativendecrements. The sum is exact (0.1plus0.2is0.3) and computed inside the decision, so the record carries it and replay reproduces it. A sum the field's kind cannot hold — outsidei64andu64for akernel/1integer, outsidei64for a service integer, no finite binary64 for a service number — is refused asCoreError::IncrementOverflow(increment_overflow) instead of reaching the schema check as a wrong type; a sum pastminormaxis stillvalidation.SetAssignment::ofreads asetvalue the way the kernel does. - Registration refuses an increment on a creation outcome (
increment_on_create), on a field that is not a declared requiredintegerornumber(increment_target_invalid), and with an amount that is not an always-present number of the field's kind (increment_amount_invalid), each with its path. - An operation's
set, and an operation outcome'sset, may write{cleared: true}under every semantics: the field is absent after the operation, and the decision and every event it emits name it inremoved— also when it was already absent, which is accepted. The schema check, invariants, events and response read the instance without it, so an invariant that needs the field refuses the operation, and replay reproduces the decision. A field with a declareddefaultmay be cleared; the default fills it at creation only. - Registration refuses a clear on a field the schema requires or does not declare (
clear_target_invalid), with a flag other thantrue(clear_flag_invalid), on a creation outcome (clear_on_create), and asetvalue naming bothclearedandincrement(set_assignment_conflict), each with its path. A cleared field also named in the outcome'sset_if_presentorfulfillsis refused asconditional_target_conflictorfulfillment_set_conflict. entity_core::recompute_createandentity_core::recompute_before_loadrecompute a recorded decision the wayreplaydoes: likecreateanddecide_before_load, except that aservice/1response is compared by the caller rather than checked against its declared schema. A verifier of stored decisions uses them; a caller taking a new decision does not.
Changed
-
A
setvalue that is a mapping whose only key isincrementis now an increment, not an object template, so anobjectorjsonfield can no longer be written with that one literal mapping; registration refuses it asincrement_target_invalid, and on a creation outcome asincrement_on_create. A mapping with any second key that is not an assignment keyword is still an object template.CoreErrorandDefinitionErrorgain variants, so an exhaustivematchon either needs new arms. -
A
setvalue that is a mapping whose only key isclearedis now a clear, not an object template, so anobjectorjsonfield can no longer be written with that one literal mapping. A mapping ofclearedandincrementand nothing else, which wrote that object into ajsonorobjectfield before, is now refused at registration asset_assignment_conflict.SetAssignmentgainsClearedandConflicting, andDefinitionErrorfour variants. -
Upgrade every process that writes a store to this release before any definition uses
{cleared: true}on ajsonorobjectfield, or any field whose schema admits the object{"cleared": true}. A build before this release reads the value as an object template and writes that literal object into the field where this release removes it; once it appends that decision, replay refuses the store. On a field the object does not fit, an older build refuses the operation instead. -
rehydrateno longer refuses everykernel/1event that carriesremoved: an operation event may name exactly the fields its operation'ssetclears. Removal evidence on a creation event, or naming a field no operation emitting the event on its transition clears, is still refused. An entity-core before 0.19.0 dropsremovedfrom an event unread, and its fold returns a cleared field still present; from 0.19.0 an older fold refuses the event. -
The OpenAPI
DomainEventschema and every AsyncAPI event messageentity generate docswrites declare the optionalremovedarray. Before, both closed the event without it, so they refused every event that carried it: a clear's, and already aservice/3fulfillmentRemove's. -
DefinitionError::ConditionalSetOnOperation(conditional_set_on_operation) is removed: every definition it refused is now admitted, andkernel/1andservice/1refuse the key earlier assemantics_key_not_available, so nothing could return it. Code that names the variant no longer compiles. A build before this release refuses a definition with an operationset_if_present, and so a history recorded under one, asconditional_set_on_operationrather than ignoring the map; open a store holding such records with this release or later. -
entity_store::projectreads a projection key of aservice/1,service/2orservice/3definition the way the kernel reads the same address. A key on<array>.count,<array>.<n>or a declared map'scount, which registration has admitted since 0.19.0, filed no instance before; it now files each instance under the array's size, that element or the map's size, an empty array under0, and leaves out an instance whose index is past the end. A map holding a member namedcountis filed under its size, no longer under that member's value.kernel/1read models are unchanged, and so is registration. -
A
service/1decision's response is checked against the command's declaredresponsebefore the decision is returned. A response member outside its declaration — a text longer than itsmax_lengthor outside itsalphabet, a number past itsmax, a wrong kind — refuses the creation or operation asvalidation, naming every offending member atresponse.<field>; before, it was answered and recorded unchecked. A decision recorded before this release whose response breaks its schema still replays throughentity_core::replayand still verifies in a recorded store:entity_store::asynchronous::validate_entry_against_staterecomputes a stored decision without the check and still compares its response byte for byte. Because the stores admit a new entry through that same function, an entry built by hand outside the kernel with such a response is admitted as before; a decision the kernel takes is checked.
Fixed
-
Under
service/1,<path>.countand<path>.<n>answer only for the kind the path's declaration names. A stored value of another kind used to answer that kind's size or member: a declaredmapholding a seven-element array counted 7, a declaredarrayholding{"count": 7}counted 7, and a declaredobjectholding an array counted its length. Each now answers nothing, so a rule reading it is unobservable. A value nothing types — an undeclared member, ajsonfield — keeps its answers. Only an instance a store kept without checking it against its definition can hold such a value. -
entity-yaml, and so theentitycommand, reads aservice/1outcome whose effect ismoveswritten as a mapping with one key,effect: { moves: { from: open, to: paid } }, the spelling JSON uses. Before, the document was refused withexpected a YAML tag starting with '!', and so was its JSON spelling given to the command, which reads JSON definitions through the same reader. A unit effect is still a plain word (effect: creates), every document the reader accepted before reads to the same definition, and the YAML tag form!moves { … }is still refused. -
entity inspect --format yamlwrites amoveseffect in that mapping form instead of the tag form!moves, so its output loads back throughentity validate.kernel/1output is unchanged. -
entity generate docswrites every number inopenapi.yamlandasyncapi.yamlas a YAML number, spelled with the digits the matching JSON contract writes. Before, each number was a mapping,$serde_json::private::Number: '1'— 24 of them in the refund example'sopenapi.yamland 14 in itsasyncapi.yaml— so a YAML reader saw an object where the contract has a number. A number no 64-bit integer or binary64 holds keeps its exact digits too. The JSON contracts are unchanged. -
Under
service/1, a rule insidefor_allorfor_anymay read a text element's length:for_all: { in: $fields.tags, as: t, that: { compare: { left: $t.count, op: lte, right: 8 } } }over a declared list of text now registers and answers each tag's length in Unicode scalar values, as does$t.name.counton a declared object element. Registration used to refuse both asQuantifierBodyScope. A quantifier element that nothing declares — its list reached through ajsonfield or a union's payload — still has no length. A declaredmapelement is still read by its members, so$g.countreads a member namedcountand not the map's size, and every recorded decision replays unchanged. -
A quantifier nested in another whose
inis an empty text, or a text opening with a character longer than one byte, is refused at registration asQuantifierOverNotCollection. Registering such a definition used to panic.