Documentation
¶
Overview ¶
Package annotation reads the documentation-adjacent facts a schema or a carrier declares — descriptions, deprecation, visibility, XML hints, extensions — and the validation-only JSON Schema keywords the IR keeps verbatim rather than models.
It exists because those two jobs share one question: given a $ref, does the fact belong to the referenced declaration or to the site that references it? Site and Home answer it once, and every reader here routes through them, so a new annotation cannot quietly pick the other answer.
Index ¶
- Constants
- Variables
- func BigValEqual(a, b ir.BigVal) bool
- func Constraints(s *oas3.Schema, exclusiveBoolean bool, pointer string, srcIndex int) (*ir.Constraints, ir.Unmodeled, []ir.Diagnostic)
- func ContainsRaw(s *oas3.Schema) (ir.RawValue, error)
- func DeclaredSchema(js *oas3.JSONSchema[oas3.Referenceable]) *oas3.JSONSchema[oas3.Referenceable]
- func DeclaresAny(s *oas3.Schema, keywords []string) bool
- func EffectiveDeprecated(ref, tgt *oas3.Schema) bool
- func EffectiveVisibility(ref, tgt *oas3.Schema) (ir.Visibility, bool)
- func ExtensionsAt(srcIndex int, sites ...ExtensionSite) (ir.Unmodeled, []ir.Diagnostic)
- func ExtensionsFrom(ext *extensions.Extensions, srcIndex int, owner string) (ir.Unmodeled, []ir.Diagnostic)
- func ExtensionsUnder(ext *extensions.Extensions, srcIndex int, owner, scope string) (ir.Unmodeled, []ir.Diagnostic)
- func FillCarrierDocs(d *ir.Docs, ref, tgt *oas3.Schema)
- func FillTypeDocs(d *ir.Docs, s *oas3.Schema)
- func IfThenElseRaw(s *oas3.Schema) (ir.RawValue, error)
- func IsFalseSchema(js *oas3.JSONSchema[oas3.Referenceable]) bool
- func MergeUnmodeled(dst, src ir.Unmodeled) ir.Unmodeled
- func PreserveInto(p *ir.Unmodeled, key string, raw ir.RawValue, reason ir.UnmodeledReason, ...)
- func PreserveKeywordInto(p *ir.Unmodeled, key string, raw ir.RawValue, declPtr, entryPtr, label string, ...) []ir.Diagnostic
- func PreserveNodeInto(p *ir.Unmodeled, key string, node *yaml.Node, reason ir.UnmodeledReason, ...) (bool, []ir.Diagnostic)
- func RawChildNode(root *yaml.Node, key string) *yaml.Node
- func RawFromNode(node *yaml.Node) (ir.RawValue, error)
- func RawPropertyNode(s *oas3.Schema, key string) *yaml.Node
- func SchemaOf(js *oas3.JSONSchema[oas3.Referenceable]) *oas3.Schema
- func UnevaluatedRaw(s *oas3.Schema) (ir.RawValue, error)
- func UnknownKeysIn(p *ir.Unmodeled, model any, srcIndex int, owner string) []ir.Diagnostic
- func UnknownKeysNamed(p *ir.Unmodeled, keys []string, root *yaml.Node, srcIndex int, ...) []ir.Diagnostic
- func UnknownKeysUnder(p *ir.Unmodeled, model any, srcIndex int, owner, scope string) []ir.Diagnostic
- func UnknownKeywordsIn(p *ir.Unmodeled, s *oas3.Schema, pointer string, srcIndex int) []ir.Diagnostic
- func UnpreservableDiag(key, pointer string, srcIndex int, err error) ir.Diagnostic
- func XMLHints(x *oas3.XML) *ir.XMLHints
- type ExtensionSite
- type Home
- type Kind
- type Set
- type Site
Constants ¶
const MaxUnknownKeys = 64
MaxUnknownKeys bounds how many keys one object contributes to the IR.
The key set is the document's to choose the size of, and every collection in this compiler is bounded, so this one is too. It sits far above what a document writes by accident, so an object reaching it is generated or hostile rather than merely sloppy, and what it discards is announced under diag.UnknownKeyBudget rather than dropped in silence.
It bounds the keys this census answers for, not the keys the object wrote: one another reader already kept is filtered out before the bound applies, since spending a slot on an entry that is in the document either way would drop a key that is not. A key that is counted but proves unreachable still spends its slot — reachability costs the same lookup as keeping it, so a bound that excluded those would have to do the work twice to decide what it bounds.
It bounds the diagnostics too, at one per key plus the budget's own, which is what keeps an object whose every key is unreachable from reporting without end.
Variables ¶
var DecidedKeywords = []string{"$comment", "$dynamicAnchor", "$dynamicRef"}
DecidedKeywords are the JSON Schema keywords the library's schema model names no field for and this compiler has already decided about, so the census must not claim them as unread. Each decision is recorded where it was made, and the schema walk's 2020-12 vocabulary test fails if one starts being carried:
- $comment — 2020-12 §8.3 forbids presenting it to end users, so no SDK emitter may see it. Dropped on purpose.
- $dynamicAnchor — read by the anchor index as a reference target, which is what lets a $dynamicRef expand; declaring one says nothing about the shape.
- $dynamicRef — carried by the dynamic-reference lowering, which either expands it into the position's type or keeps it under a reason of its own. An entry beside an expanded one would tell a consumer the compiler ignored a reference it had in fact resolved.
The other 2020-12 keywords with no field of their own — $vocabulary and dependentRequired — need no entry here. Their readers write to the same map, so the census finds them already recorded and leaves them alone.
var DialectKeywords = []string{"$id", "$schema", "$vocabulary"}
DialectKeywords are the JSON Schema resource and dialect keywords the IR excludes on purpose. It identifies every type by a synthetic ID derived from its source pointer rather than by `$id` (ir-design §3), and describes one API surface rather than a JSON Schema resource graph, so it has no dialect axis for `$schema`/`$vocabulary` to land on and none is coming — ReasonOutOfScope rather than ReasonNoIRHome (§12).
`$id` is kept, not honoured: reference resolution addresses same-document JSON pointers, never an `$id` base URI.
Functions ¶
func BigValEqual ¶
BigValEqual reports whether two numeric literals denote the same value, so that one magnitude spelled two ways — 10, 10.0, 1e1 — is one value rather than two.
It reads each literal rather than materializing it, and that is what makes it total. A rational will not build 1e1000001 at all, so a comparison resting on one has to answer that pair some other way, and the only answer left is their text — which calls 1e1000001 and 10e1000000 two values and reports a disagreement between a bound and itself.
Identical text is the one pair that needs no reading: a literal denotes one value whatever grammar it belongs to. That is also the whole answer for a pair outside the decimal grammar, where two differing spellings are reported unequal rather than guessed at. No BigVal is outside that grammar today, and TestBigValGrammarStaysWithinTheDecimalReading is what holds that true.
func Constraints ¶
func Constraints(s *oas3.Schema, exclusiveBoolean bool, pointer string, srcIndex int) (*ir.Constraints, ir.Unmodeled, []ir.Diagnostic)
Constraints reads a schema's scalar (string/number/object-count) value constraints into an ir.Constraints. Numeric bounds are read from the raw YAML nodes, never the *float64 model fields, to preserve full decimal precision (the no-float64 invariant). Collection bounds (minItems/maxItems/uniqueItems) are List-owned and read elsewhere. A non-finite bound literal yields an error-severity diag.NumericPrecision diagnostic and is skipped; nil is returned when no constraint is present. exclusiveBoolean selects the exclusiveMinimum/exclusiveMaximum dialect (see applyExclusive), and under the 2020-12 one a side that declares both of its keywords is settled by reconcileBound rather than by whichever ran last.
The keyword that reconciliation leaves out of ir.Constraints comes back as the second return, an ir.Unmodeled the caller merges into whichever carrier its reading position owns. pointer and srcIndex locate it, exactly as they locate what Read keeps. Everything else a schema says about its values reaches a field, so on all but a co-declared numeric bound that map is nil.
It reads beside the other readers here for the reason they are here at all: what a schema says about the values admitted at a position is read the same way whoever asks, and none of it needs the lowering walk. Which dialect applies is the caller's to decide — that is a fact about the document, not about the schema, and it is the one thing this reader will not go and find.
func ContainsRaw ¶
ContainsRaw combines contains/minContains/maxContains into one raw JSON object.
func DeclaredSchema ¶
func DeclaredSchema(js *oas3.JSONSchema[oas3.Referenceable]) *oas3.JSONSchema[oas3.Referenceable]
DeclaredSchema returns the schema written at the position js references — one hop, not the end of the chain. GetResolvedSchema follows a reference to a reference all the way through, which is the wrong node to hoist at that position: a sub-schema spelled {$ref: Other, minimum: 7} would be read as Other, and the bound written beside the $ref would be gone before anything could record it.
func DeclaresAny ¶
DeclaresAny reports whether s writes any of keywords. It reads the raw nodes rather than the model fields for the reason declaresValueConstraints does: the recorders these gate (recordResidue, dialectAt) read them there, and a predicate consulting a different source could disagree with them in either direction.
func EffectiveDeprecated ¶
EffectiveDeprecated reports the deprecated flag, use-site over referent.
func EffectiveVisibility ¶
func EffectiveVisibility(ref, tgt *oas3.Schema) (ir.Visibility, bool)
EffectiveVisibility maps readOnly/writeOnly to a lifecycle visibility set (ir-design §5.2): readOnly is present in every response lifecycle (read/delete/query) and absent only from requests; writeOnly is create+update. It reports separately whether both flags were in force, which no lifecycle satisfies.
Each flag is resolved on its own, use-site over referent, so a position that writes one of them settles that flag and leaves the other to resolve from the referent — the uniform §14 merge, not a composite annotation one node wins outright.
Both in force is contradictory but legal: JSON Schema 2020-12 says readOnly means the value is not writable and writeOnly that it is not readable, and forbids neither beside the other. Read as sets, they leave nothing — the property is admitted by no lifecycle, which Visibility{None: true} states exactly. That is what merge.mergeVisibility already answers when the same pairing is spread over two allOf branches, so the two spellings of one contradiction no longer disagree (GitHub #276). Guarding readOnly first and returning is what made them disagree, and it discarded the second flag with no diagnostic in either channel.
The bool rather than a diagnostic: this reader has no provenance of its own, and the caller that has one is the caller that knows which carrier it is filling.
func ExtensionsAt ¶
func ExtensionsAt(srcIndex int, sites ...ExtensionSite) (ir.Unmodeled, []ir.Diagnostic)
ExtensionsAt folds every site into one Unmodeled map, for the carriers that hold more than one object's extensions. Sites are applied in the order given, which is source order at every caller; distinct scopes cannot collide, so the order decides nothing but is fixed anyway.
func ExtensionsFrom ¶
func ExtensionsFrom(ext *extensions.Extensions, srcIndex int, owner string) (ir.Unmodeled, []ir.Diagnostic)
ExtensionsFrom lowers an x-* extension map into namespaced ir.Unmodeled, keys prefixed "openapi:" and values serialized to raw JSON. owner is the pointer of the object the extensions were written on; each entry is located at its own key beneath it and marked ReasonVendorExtension, since the format assigns an x-* key no semantics at all.
func ExtensionsUnder ¶
func ExtensionsUnder(ext *extensions.Extensions, srcIndex int, owner, scope string) (ir.Unmodeled, []ir.Diagnostic)
ExtensionsUnder is ExtensionsFrom with every entry keyed beneath scope, for the objects whose extensions have no Unmodeled map of their own to land on.
Most OpenAPI objects lower to an IR node that carries one, and those pass an empty scope. The rest ride on the nearest node that does — an info object's on the document, an encoding's on the content, a path item's on each of its operations — and several of them can reach the same map, where "openapi:x-id" from two objects is one key and the surviving entry would depend on which lowering ran last. scope names which object wrote them: the source path from the carrier down to it, or the object's own keyword where it is not beneath the carrier at all.
No two keys can collide: every scope segment is a literal that never begins with "x-" and every extension name always does, so the first "x-" segment is where the scope ends and the name begins, and one key cannot be spelled by two (scope, owner) pairs. That same gap holds against the non-extension keys a carrier already holds under these scopes, such as the "openapi:encoding/<part>/allowReserved" written beside an encoding's x-*.
func FillCarrierDocs ¶
FillCarrierDocs fills the ir.Property or ir.Parameter carrying a position with the documentation effective there: the $ref referent's title, description and externalDocs first, then the use-site's over them, field by field. A carrier therefore ends up with documentation the position itself need not have written — a bare `$ref` reads all three from the referent, which keeps its own copy on its node.
Both halves are deliberate. The use-site half is the only home a keyword written *at* the position has once the body reduced to a shared node (GitHub #116): dropping it there loses it outright. The referent half is ir-design §14 — ref-target annotations merge onto the referencing Property/Parameter with use-site precedence, applied uniformly — and it is how every other field this compiler reads through a $ref already behaves (fillPropertyDefault, EffectiveVisibility, EffectiveDeprecated).
Uniform is the load-bearing word: description alone inheriting, while title and externalDocs stop at the position, is the ad-hoc per-keyword patching §14 names as the counterexample to avoid.
func FillTypeDocs ¶
FillTypeDocs maps a schema's title, description, and externalDocs onto Docs. Every field is assigned, never accumulated: a schema declares at most one externalDocs, and a pointer read twice (a sub-schema lowered both in place and through a $ref that hoists it) must not end up with two copies of it.
func IfThenElseRaw ¶
IfThenElseRaw combines the present if/then/else arms into one raw JSON object.
func IsFalseSchema ¶
func IsFalseSchema(js *oas3.JSONSchema[oas3.Referenceable]) bool
IsFalseSchema reports whether js is the boolean `false` schema.
func MergeUnmodeled ¶
MergeUnmodeled overlays src onto dst, allocating dst on first write.
func PreserveInto ¶
func PreserveInto(p *ir.Unmodeled, key string, raw ir.RawValue, reason ir.UnmodeledReason, pointer string, srcIndex int, )
PreserveInto records raw under key in p, or records nothing when there are no bytes to record.
len rather than a nil comparison: nil and a zero-length slice are distinct states, and an empty payload is the worse of the two. It preserves no construct, and json.Marshal rejects it for the whole document while naming json.RawMessage rather than the entry that carried it.
func PreserveKeywordInto ¶
func PreserveKeywordInto(p *ir.Unmodeled, key string, raw ir.RawValue, declPtr, entryPtr, label string, srcIndex int, ) []ir.Diagnostic
PreserveKeywordInto records a validation-only keyword and returns the one diagnostic announcing it. An absent payload records nothing and announces nothing; an unconvertible one never reaches here, because its caller reports it through UnpreservableDiag first.
func PreserveNodeInto ¶
func PreserveNodeInto(p *ir.Unmodeled, key string, node *yaml.Node, reason ir.UnmodeledReason, pointer string, srcIndex int, ) (bool, []ir.Diagnostic)
PreserveNodeInto converts node and records it under key, reporting a construct that reached the IR in no form at all rather than leaving its caller to announce a preservation that did not happen (GitHub #144).
It reports whether an entry was written, which is what a caller with an announcement to make gates on. The three outcomes it distinguishes are the point: an absent node writes nothing and says nothing, because there was no construct; a converted one writes the entry; an unconvertible one writes nothing and yields the diagnostic that says so.
func RawChildNode ¶
RawChildNode returns the raw YAML value node of a mapping child keyed by the on-wire name, unwrapping a document node first; nil when absent. It reads exact literals the high-level model does not preserve (links, servers, content maps).
The last pair spelling the key wins, which is the pair the parser reads: marshaller skips every occurrence of a repeated key but the last. Returning the first instead described a mapping by a value nothing else in the compiler uses — reachable once a key can be spelled two ways, since an explicit pair and an aliased one are one key to the parser and two nodes here.
func RawFromNode ¶
RawFromNode converts a YAML node to the canonical JSON an Unmodeled entry holds.
Its three outcomes are deliberately distinct, because collapsing the first two into one nil return is what let a diagnostic announce a preservation that never happened (GitHub #144): an absent node yields (nil, nil) — there was no construct here — while a node that cannot be represented yields an error.
A node fails to convert when it names something JSON cannot: a mapping key that is not a string, a key written twice, .nan or .inf, or a scalar whose tag promises a type its text does not hold. The walk's own bounds refuse two shapes more — an alias that cycles, and one that expands past its node budget. A tag yaml.v3 assigns no type to is not a failure: its scalar keeps its text.
The conversion walks the node tree rather than decoding it into `any` and re-marshalling, because that decode rounds every numeric literal through float64: it silently rewrote a 23-digit extension value and flattened 1.000000000000000000001 to 1, in the one channel whose whole promise is verbatim preservation (GitHub #32).
func RawPropertyNode ¶
RawPropertyNode returns the raw YAML value node of a top-level schema keyword, or nil when absent. The library's GetPropertyNode resolves Go core field names and returns key nodes; this scans the schema's root mapping for the on-wire keyword and returns its value node, which is where exact literals live.
func SchemaOf ¶
func SchemaOf(js *oas3.JSONSchema[oas3.Referenceable]) *oas3.Schema
SchemaOf returns the schema body written at this position, including one that also carries a $ref — an example or bound written beside a $ref binds the position, not the referent, the same rule fillPropertyAnnotations and fillPropertyConstraints apply at a property. It returns nil only where no body is written: a nil either, or a boolean schema, which admits no annotations.
Every position modeled as a site reads it through At: a named component (lowerComponentSchema) and a $ref'd internal sub-schema (hoistSubSchema, fed one hop at a time by DeclaredSchema).
func UnevaluatedRaw ¶
UnevaluatedRaw combines a non-false unevaluatedProperties and any unevaluatedItems into one raw JSON object (a false unevaluatedProperties is a structural mode, handled in fillAdditional).
func UnknownKeysIn ¶
UnknownKeysIn records on p the keys an OpenAPI object writes that the specification neither defines nor admits as an extension, for an object lowering to a node with an Unmodeled map of its own. owner is the object's own source pointer.
Unlike its schema neighbour this reports a fault: OpenAPI gives each of its objects a closed key set and requires every extension to be prefixed x-, so a key that is neither is nothing the document is permitted to write — in practice a misspelling of the field beside it. It is kept all the same, because invariant 2 does not bend for invalid input, and a misspelt key is the one a reader most needs to find.
func UnknownKeysNamed ¶
func UnknownKeysNamed(p *ir.Unmodeled, keys []string, root *yaml.Node, srcIndex int, owner, scope string, ) []ir.Diagnostic
UnknownKeysNamed is UnknownKeysUnder for an object whose model keeps no census of its own, so the caller names the keys and hands over the mapping node they were written on.
One object needs it: a Path Item Object, whose core model embeds the map of its operations. The unmarshaller folds a key it does not recognize into that embedded map rather than recording it as undeclared, so the object's own census is empty however much the document wrote (speakeasy-api/openapi v1.24.1). Its leftovers are still an undeclared key of the path item, graded as one — same code, same severity, same reason — because which reader found them is not a property of the source.
It delegates rather than duplicating the grading, so the two can only be announced alike.
func UnknownKeysUnder ¶
func UnknownKeysUnder(p *ir.Unmodeled, model any, srcIndex int, owner, scope string) []ir.Diagnostic
UnknownKeysUnder is UnknownKeysIn with every entry keyed beneath scope, for the objects with no Unmodeled map of their own, whose keys ride on the nearest node that has one — an info object's on the document, a tag's on the document.
scope says which object wrote them: the source path from the carrier down to the object. Several objects reach one map, where "openapi:status" from two of them would be a single key and the entry that survived would depend on which lowering ran last.
func UnknownKeywordsIn ¶
func UnknownKeywordsIn(p *ir.Unmodeled, s *oas3.Schema, pointer string, srcIndex int) []ir.Diagnostic
UnknownKeywordsIn records on p the keywords s writes that no field of the JSON Schema model names, and announces each.
OpenAPI 3.1 schemas are JSON Schema 2020-12, where an unrecognized keyword is legal input: the specification requires an implementation to ignore what it does not recognize and allows such a keyword to carry meaning for other tooling. So this reports a decision rather than a fault, and is graded accordingly — see diag.UnknownSchemaKeyword.
It keeps only what no other reader kept, which is why it runs after all of them: `$vocabulary` and `dependentRequired` have no field in the model either and are read straight off the raw node by readers with more to say about them, so the census finds those already recorded and leaves them alone. A keyword no reader leaves a trace of needs naming in DecidedKeywords instead.
func UnpreservableDiag ¶
func UnpreservableDiag(key, pointer string, srcIndex int, err error) ir.Diagnostic
UnpreservableDiag reports a construct the compiler could neither model nor keep verbatim, so nothing of it reached the IR.
Error rather than the degradation severity its callers otherwise use: a degraded lowering still describes the source in a weaker shape, while this one leaves no trace at all, which is a losslessness failure rather than a compromise (GitHub #144).
The vendor-extension reader (ExtensionsFrom) reports the same conversion failure as a warning and is deliberately left alone: it already branches on the error and never claimed to have kept anything, so it is not the defect this code exists for.
Types ¶
type ExtensionSite ¶
type ExtensionSite struct {
Scope string
Owner string
Ext *extensions.Extensions
}
ExtensionSite is one object's x-* map paired with where it was written: Owner is the object's own source pointer, and Scope is what its entries key under on the carrier that ends up holding them (see ExtensionsUnder).
type Home ¶
type Home int
Home names where the annotations a schema position declares are kept. It is what decides whether a position that lowered to a shared node hoists one of its own, so the two homes can never both hold the same declaration (GitHub #116).
const ( // HomeOwnNode marks a position with no home but a type node: items, // additionalProperties, prefixItems, patternProperties, a union branch, a // media-type schema, a component, a $ref-hoisted sub-schema. A declaration // there hoists an alias rather than lose what it wrote. HomeOwnNode Home = iota // HomeCarrier marks a position whose caller carries an ir.Property or // ir.Parameter that holds the declaration's annotations itself // (fillPropertyDetail, fillParamSchema): a model property, a response or // part header, an operation parameter. Hoisting there would give one // declaration two homes. HomeCarrier )
type Kind ¶
type Kind int
Kind distinguishes a position that declares a type from one that references another type and may carry annotations of its own.
The two site kinds. Declaration is a position that writes a schema of its own; Reference is one that points at another and may still annotate the pointer.
type Set ¶
type Set struct {
Docs ir.Docs
Deprecated bool
XML *ir.XMLHints
Examples []ir.Example
Unmodeled ir.Unmodeled
}
Set is everything a site's annotations yield, before any of it is attached to a carrier.
The readers produce a value; the caller decides what its carrier can hold. A Parameter has no XML field and a TypeCommon has no Required, so the shape of the write differs per carrier while the reading does not.
func Read ¶
Read reads every site-local annotation at st.
This is the single call site the decomposition exists for. Not because it merges duplicate readers — the docs readers are genuinely distinct and stay distinct — but because the site-versus-referent choice is made here once instead of at each position that attaches annotations. Three attachment points previously made it separately and disagreed: one passed a referent, one passed nil because a declaration has none, and one passed nil because it never resolved the referent it had.
type Site ¶
type Site struct {
Kind Kind
Node *oas3.Schema
// Referent is nil when Kind is Reference but the $ref does not
// resolve; refTypeRef is what diagnoses that, not this.
Referent *oas3.Schema
}
Site is what a schema position declares: Node is the schema written there, and Referent — set only for a reference site — is the schema exactly one hop away, never the end of a $ref chain.
The split lets an annotation be read from where it was written rather than wherever the $ref resolves to, with a fallback to Referent for annotations meant to inherit from the target. Only Node has a production reader: a declaration's annotations bind the position they are written at (attachDeclaredAnnotations), and a component that aliases another keeps the target's own annotations reachable through its Base rather than copying them. Kind and Referent are for a reader that does want to inherit. fillPropertyDetail (schema.go) instead falls back via refTargetSchema, which follows a $ref chain to its end (GetResolvedSchema) rather than one hop (GetReferenceResolutionInfo, what Referent uses). The two are not interchangeable: swapping one for the other would silently change property-default and description semantics on a ref-to-ref chain.
func At ¶
func At(js *oas3.JSONSchema[oas3.Referenceable]) Site
At builds the site for js. A $ref position resolves Referent exactly one hop through DeclaredSchema, never the full chain — see Site and DeclaredSchema for why that distinction matters.
A schema whose $ref pointer is present but empty ({$ref: ""}) is not a reference site: an empty ref resolves nowhere, so IsReference is false and it is classified as a declaration like any other schema body. That is what keeps Referent's nil guarantee true: a reference site's $ref was genuinely attempted, so an unresolved target is the only reason Referent is nil.
At trusts its caller that js is genuinely the schema at the position being modeled; nothing here can cross-check that from js alone.