Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions docs/ir-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,10 @@ is not, so a single ill-formed name, ID or description fails the whole artifact.
every string to it under one code, `ir/invalid-utf8`, at the path of the string that broke it.

**`Aliases` is held to none of the shape rules, and to rules of its own instead.** An alias is
matched against a name *another* schema wrote — an Avro alias is a full name, `com.example.User`,
and resolution compares it verbatim against the writer schema's full name — so the separators and
the casing are what the match is made of rather than a spelling the IR gets to decide.
matched against a name *another* schema wrote — an Avro type alias is a full name such as
`com.example.User`, or one relative to its type's namespace, and resolution compares it against the
writer schema's full name — so the separators and the casing are what the match is made of rather
than a spelling the IR gets to decide.
Neutralizing one would throw away precisely that, which is the lossy direction
lossless-by-default rules out. `Source` is the internal precedent: it carries `UserID` today and
no content rule touches it, because it records what the spec said rather than deciding a spelling.
Expand All @@ -444,6 +445,20 @@ That such an entry is inert is also why a *source* declaring one is recorded onc
rather than carried through: dropping it is not the lossy direction, since the same set of names
resolves to the entity either way.

An alias can also collide with *another* entity's name, and that is not a rule of the list:

- **Within a record** it is a problem in the spec. An alias that another property of the same model
answers to — by its wire name or by an alias of its own — leaves a reader resolving that name two
fields to choose from, which is why Avro holds an alias to the same uniqueness as a name. A source
can write one, and no lowering can drop either claim without choosing between them, so
`pass/validate` reports it (`pass/ambiguous-alias`) beside the duplicate wire names of the same
scope, and `irverify` does not.
- **Across the type registry** nothing reports it, because nothing in the IR decides it. A relative
alias is qualified by its type's namespace under the format's grammar, so one string on two types
in two namespaces is two names; and one document can hold several independent schemas — each
message payload of an AsyncAPI document is one — that may each declare the same type and alias.
A collision between named types is the compiler's to report, within the schema it parsed.

### 3.3 Type references

```go
Expand Down Expand Up @@ -520,6 +535,12 @@ type TemplateArg struct {
}
```

`Namespace` is a path, one segment per level, as `Service.Namespace` and `GraphQLBinding.FieldPath`
(§8.4) are. **No segment is blank** (`ir/path-segment-blank`): no namespace or field grammar has an
empty level, so a source that spells one has written an invalid name, which its compiler reports
rather than carries. A null or global namespace is the empty list, not one blank segment. A segment
may repeat — `com.acme.acme` has three levels — so a path is not held to unique entries.

### 4.1 Primitives

```go
Expand Down Expand Up @@ -1215,6 +1236,10 @@ type Visibility struct {
}
```

`Only`, like an operation's `ParameterVisibility` and `ReturnTypeVisibility` (§7.2), is a set: no
entry is blank and none repeats (`ir/set-entry-blank`, `ir/set-entry-duplicate`), since a blank
names no class and a repeat admits nothing more.

`readOnly` lowers to `Only: [read]` (plus delete/query per OpenAPI semantics); `writeOnly` to
`Only: [create, update]`; GraphQL input-vs-output types to `create/update` vs `read`; TypeSpec
`@visibility` maps directly. One logical model therefore produces N wire shapes; the projection
Expand Down
3 changes: 2 additions & 1 deletion ir/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ type Reply struct {
type GraphQLBinding struct {
// Kind is "query" | "mutation" | "subscription".
Kind string `json:"kind,omitempty"`
// FieldPath is the entry-point field (nesting for namespaced schemas).
// FieldPath is the entry-point field (nesting for namespaced schemas). It is
// a path: no segment is blank, and one may repeat (ir-design §4).
FieldPath []string `json:"fieldPath,omitempty"`
// Unmodeled holds source constructs the IR does not model, kept verbatim.
Unmodeled Unmodeled `json:"unmodeled,omitempty"`
Expand Down
3 changes: 2 additions & 1 deletion ir/irverify/duplicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package irverify

import (
"reflect"
"strconv"
"strings"

"github.com/dexpace/morphic/ir"
Expand Down Expand Up @@ -58,7 +59,7 @@ func checkDuplicateIDs(doc *ir.Document, decls declarations) ([]Violation, bool)
}
vs = append(vs, Violation{
Code: "ir/duplicate-" + ir.RefNoun(d.Class) + "-id",
Message: "id " + d.ID + " is declared here and at " + at.path,
Message: "id " + strconv.Quote(d.ID) + " is declared here and at " + strconv.Quote(at.path),
Path: d.Path,
})
}
Expand Down
18 changes: 17 additions & 1 deletion ir/irverify/export_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package irverify

import "testing"
import (
"maps"
"testing"
)

// ValueCarriers exposes valueCarriers to this package's external tests.
//
Expand All @@ -19,3 +22,16 @@ func SelfReachableReferences(t *testing.T) []string {
t.Helper()
return selfReachableReferences(t)
}

// HeldLists exposes, for each field stringLists holds to a rule, whether that
// rule is listSet rather than listPath, so the external tests' fixtures can be
// held to reaching every one with the right expectation.
func HeldLists() map[string]bool {
held := map[string]bool{}
for key, rule := range maps.All(stringLists) {
if rule == listPath || rule == listSet {
held[key] = rule == listSet
}
}
return held
}
15 changes: 8 additions & 7 deletions ir/irverify/ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package irverify
import (
"encoding/json/jsontext"
"reflect"
"strconv"
"strings"

"github.com/dexpace/morphic/ir"
Expand Down Expand Up @@ -138,10 +139,10 @@ func checkPrimIDs(doc *ir.Document) []Violation {
// — checkIDs reports it malformed wherever it is used — so offering it as the
// place the node belongs would send a reader to fix the wrong end.
func primIDViolation(id ir.TypeID, kind ir.PrimKind, want ir.TypeID, path string) Violation {
msg := "primitive of kind " + string(kind) + " is interned at " + string(id) +
" rather than the shared " + string(want)
msg := "primitive of kind " + strconv.Quote(string(kind)) + " is interned at " + strconv.Quote(string(id)) +
" rather than the shared " + strconv.Quote(string(want))
if kind == "" {
msg = "primitive at " + string(id) + " carries no kind, so no shared ID derives from it"
msg = "primitive at " + strconv.Quote(string(id)) + " carries no kind, so no shared ID derives from it"
}
return Violation{Code: "ir/prim-id-not-derived", Message: msg, Path: path}
}
Expand All @@ -160,7 +161,7 @@ func appendReservedSpace(vs []Violation, id ir.TypeID, kind ir.TypeKind, path st
}
return append(vs, Violation{
Code: "ir/prim-space-reserved",
Message: "id " + string(id) + " addresses the reserved primitive space but names a " +
Message: "id " + strconv.Quote(string(id)) + " addresses the reserved primitive space but names a " +
string(kind),
Path: path,
})
Expand All @@ -171,7 +172,7 @@ func appendIDViolations(vs []Violation, kind, id string, prov ir.Provenance, pat
if !ir.WellFormedID(kind, id) {
return append(vs, Violation{
Code: "ir/id-malformed",
Message: "id " + id + " is not " + kind + "/<space>[/<path>]; every segment must be non-empty",
Message: "id " + strconv.Quote(id) + " is not " + kind + "/<space>[/<path>]; every segment must be non-empty",
Path: path,
})
}
Expand All @@ -185,8 +186,8 @@ func appendIDViolations(vs []Violation, kind, id string, prov ir.Provenance, pat
}
return append(vs, Violation{
Code: "ir/id-provenance-disagreement",
Message: "id " + id + " carries path " + idPath +
", which is not the source pointer " + string(prov.Pointer) + " it records",
Message: "id " + strconv.Quote(id) + " carries path " + strconv.Quote(idPath) +
", which is not the source pointer " + strconv.Quote(string(prov.Pointer)) + " it records",
Path: path,
})
}
8 changes: 6 additions & 2 deletions ir/irverify/irverify.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package irverify
import (
"cmp"
"slices"
"strconv"

"github.com/dexpace/morphic/ir"
)
Expand All @@ -13,7 +14,9 @@ import (
type Violation struct {
// Code is a stable, slash-namespaced identifier for the defect class.
Code string
// Message is a human-readable description of the specific defect.
// Message is a human-readable description of the specific defect. Text it
// takes from the document is strconv.Quote'd, so it is valid UTF-8 whatever
// the document holds (GitHub #400); Path is not yet (GitHub #739).
Message string
// Path locates the offending node within the document.
Path string
Expand Down Expand Up @@ -82,6 +85,7 @@ func walkChecks() []func(*ir.Document, declarations) ([]Violation, bool) {
checkDeclaredIDs,
checkIDSpaces,
checkNaming,
checkStringLists,
checkRawPayloads,
checkProvenance,
checkIndices,
Expand Down Expand Up @@ -171,7 +175,7 @@ func registryKey(vs []Violation, noun, reg, key, nodeID string) []Violation {
if key != nodeID {
return append(vs, Violation{
Code: "ir/" + noun + "-id-mismatch",
Message: "registry key " + key + " disagrees with node ID " + nodeID,
Message: "registry key " + strconv.Quote(key) + " disagrees with node ID " + strconv.Quote(nodeID),
Path: reg + "[" + key + "]",
})
}
Expand Down
5 changes: 3 additions & 2 deletions ir/irverify/memberids.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ func memberScopeViolation(enumID ir.TypeID, member ir.EnumMemberID, i int) (Viol
}
return Violation{
Code: "ir/member-id-scope",
Message: "member id " + string(member) + " is not in the namespace " + enumSpace +
" and beneath the path " + enumPath + " of its enum " + string(enumID),
Message: "member id " + strconv.Quote(string(member)) + " is not in the namespace " +
strconv.Quote(enumSpace) + " and beneath the path " + strconv.Quote(enumPath) +
" of its enum " + strconv.Quote(string(enumID)),
Path: ir.DocumentPath + ".Types[" + string(enumID) + "].Members[" + strconv.Itoa(i) + "]",
}, true
}
Loading
Loading