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
64 changes: 50 additions & 14 deletions docs/doc-site/shaping-the-output/names-and-refs/alias-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,56 @@ site the alias **dissolves** to its target, producing no definition of its own.

## Exposing an alias as a first-class entity

This is an **advanced, rarely-needed** case. To keep the alias name in the spec
(its own definition that other schemas `$ref`), annotate the alias with
`swagger:model`. Two top-level options then govern how that first-class alias
*definition* is shaped:

- **default (expand)** — the alias definition is a structural copy of the
target.
- **`RefAliases: true`** — the alias definition is a `$ref` chain to the target
(`{"$ref": "#/definitions/Money"}`), preserving the alias name at use sites.
- **`TransparentAliases: true`** — aliases dissolve to their target everywhere,
overriding the per-declaration annotation (use sites become `$ref` to the
target, as in the default-dissolve example above).

The canonical witnesses are the `fixtures/enhancements/alias-calibration-embed`
This is an **advanced, rarely-needed** case. To keep the alias name in the spec —
its own definition that other schemas `$ref` — annotate the alias with
`swagger:model`:

{{< code file="shaping/aliases-firstclass/firstclass.go" lang="go" region="firstclass" >}}

Two top-level options then govern how that first-class alias *definition* is
shaped. The panes below are the same package scanned under each.

### Default — the alias definition is a copy

`Fee` is emitted as a structural duplicate of `Amount`, and `Receipt.charge`
points at the alias:

{{< compare
left="shaping/aliases-firstclass/testdata/expand.json" leftlabel="Default (expand)"
right="shaping/aliases-firstclass/testdata/refaliases.json" rightlabel="RefAliases: true" >}}

### `RefAliases: true` — the alias definition is a `$ref` chain

The right pane above: `Fee` becomes `{"$ref": "#/definitions/Amount"}`. One
shape, two names — the alias survives at use sites without duplicating the
target's properties. Prefer this over the default whenever the alias is
genuinely a synonym: a copy drifts the moment the target changes.

### `TransparentAliases: true` — use sites dissolve

{{< code file="shaping/aliases-firstclass/testdata/transparent.json" lang="json" >}}

`Receipt.charge` now points straight at `#/definitions/Amount`: the alias is
gone from the reference graph.

{{% notice style="warning" %}}
Note what did **not** happen: `Fee` is still emitted. `TransparentAliases`
governs how an alias renders at its *use sites*, not whether an annotated
declaration produces a definition — so with `ScanModels` you get a `Fee`
definition that nothing references. Add
[`PruneUnusedModels`]({{% relref "pruning-unused-models" %}}) to drop it, or
simply do not annotate an alias you intend to dissolve.
{{% /notice %}}

The three modes at a glance:

| | `Fee` definition | `Receipt.charge` |
|---|---|---|
| default (expand) | copy of `Amount` | `$ref: Fee` |
| `RefAliases: true` | `$ref: Amount` | `$ref: Fee` |
| `TransparentAliases: true` | copy of `Amount`, unreferenced | `$ref: Amount` |

Wider calibration lives in the `fixtures/enhancements/alias-calibration-embed`
golden trio.

{{% notice style="note" %}}
Expand Down
39 changes: 39 additions & 0 deletions docs/examples/shaping/aliases-firstclass/firstclass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0

// Package firstclass holds the annotated declarations used by the "first-class
// alias" half of the "Alias rendering" how-to — an alias that keeps its own
// identity in the spec, and the two options that shape it.
//
// firstclass_test.go scans this package three times, once per alias mode, and
// writes one golden per mode, so the guide compares real output rather than
// describing it.
package firstclass

// snippet:firstclass

// Amount is the underlying model.
//
// swagger:model
type Amount struct {
// Cents is the amount in cents.
Cents int64 `json:"cents"`

// Currency is the ISO currency code.
Currency string `json:"currency"`
}

// Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name
// in the spec instead of dissolving it to Amount.
//
// swagger:model
type Fee = Amount

// Receipt references the alias, not the target.
//
// swagger:model
type Receipt struct {
// Charge is the fee charged.
Charge Fee `json:"charge"`
}

// endsnippet:firstclass
118 changes: 118 additions & 0 deletions docs/examples/shaping/aliases-firstclass/firstclass_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0

package firstclass

import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/go-openapi/codescan"
"github.com/go-openapi/spec"
"github.com/go-openapi/testify/v2/assert"
"github.com/go-openapi/testify/v2/require"
)

func examplesRoot(t *testing.T) string {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
require.True(t, ok)

return filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", ".."))
}

// scanMode scans the package under one alias mode.
func scanMode(t *testing.T, opts codescan.Options) spec.Definitions {
t.Helper()
opts.WorkDir = examplesRoot(t)
opts.Packages = []string{"./shaping/aliases-firstclass"}
opts.ScanModels = true

doc, err := codescan.Run(&opts)
require.NoError(t, err)
require.NotNil(t, doc)

return doc.Definitions
}

// goldenDefs emits and verifies the whole definitions map for one mode.
func goldenDefs(t *testing.T, feature string, defs spec.Definitions) {
t.Helper()
got, err := json.MarshalIndent(defs, "", " ")
require.NoError(t, err)
got = append(got, '\n')

golden := filepath.Join("testdata", feature+".json")
if os.Getenv("UPDATE_GOLDEN") != "" {
require.NoError(t, os.WriteFile(golden, got, 0o600))
}
want, err := os.ReadFile(golden)
require.NoError(t, err)
assert.JSONEq(t, string(want), string(got))
}

// refOf returns the $ref carried by a property, or "" when it carries none.
func refOf(defs spec.Definitions, def, prop string) string {
prd := defs[def].Properties[prop]

return prd.Ref.String()
}

// TestFirstClassAlias_Expand locks the default: the alias definition is a
// structural COPY of its target, and use sites point at the alias.
//
// Regenerate with: UPDATE_GOLDEN=1 go test ./...
func TestFirstClassAlias_Expand(t *testing.T) {
defs := scanMode(t, codescan.Options{})

require.Contains(t, defs, "Fee")
assert.Contains(t, defs["Fee"].Properties, "cents",
"the default expands the alias into a copy of the target")
feeDefault := defs["Fee"]
assert.Empty(t, feeDefault.Ref.String(), "no $ref chain in the default mode")
assert.Equal(t, "#/definitions/Fee", refOf(defs, "Receipt", "charge"),
"the use site keeps the alias name")

goldenDefs(t, "expand", defs)
}

// TestFirstClassAlias_RefAliases locks RefAliases: the alias definition becomes a
// $ref chain to its target rather than a copy — one shape, two names.
func TestFirstClassAlias_RefAliases(t *testing.T) {
defs := scanMode(t, codescan.Options{RefAliases: true})

fee := defs["Fee"]
assert.Equal(t, "#/definitions/Amount", fee.Ref.String(),
"the alias definition is a $ref chain to the target")
assert.Empty(t, defs["Fee"].Properties, "no duplicated properties under RefAliases")
assert.Equal(t, "#/definitions/Fee", refOf(defs, "Receipt", "charge"),
"the use site still keeps the alias name")

goldenDefs(t, "refaliases", defs)
}

// TestFirstClassAlias_Transparent locks the part that surprises: TransparentAliases
// dissolves the alias at USE SITES, but a swagger:model-annotated alias is still
// published as its own definition under ScanModels — it just stops being
// referenced by anything.
func TestFirstClassAlias_Transparent(t *testing.T) {
defs := scanMode(t, codescan.Options{TransparentAliases: true})

assert.Equal(t, "#/definitions/Amount", refOf(defs, "Receipt", "charge"),
"the use site dissolves to the target")
require.Contains(t, defs, "Fee",
"the annotated alias is still published; the option governs use sites, not the annotation")
assert.Contains(t, defs["Fee"].Properties, "cents")

// Nothing references Fee any more — with PruneUnusedModels it would be dropped.
for _, def := range defs {
for name, prop := range def.Properties {
assert.NotEqualf(t, "#/definitions/Fee", prop.Ref.String(),
"property %q still points at the dissolved alias", name)
}
}

goldenDefs(t, "transparent", defs)
}
48 changes: 48 additions & 0 deletions docs/examples/shaping/aliases-firstclass/testdata/expand.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"Amount": {
"type": "object",
"title": "Amount is the underlying model.",
"properties": {
"cents": {
"description": "Cents is the amount in cents.",
"type": "integer",
"format": "int64",
"x-go-name": "Cents"
},
"currency": {
"description": "Currency is the ISO currency code.",
"type": "string",
"x-go-name": "Currency"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
},
"Fee": {
"description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
"type": "object",
"properties": {
"cents": {
"description": "Cents is the amount in cents.",
"type": "integer",
"format": "int64",
"x-go-name": "Cents"
},
"currency": {
"description": "Currency is the ISO currency code.",
"type": "string",
"x-go-name": "Currency"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
},
"Receipt": {
"type": "object",
"title": "Receipt references the alias, not the target.",
"properties": {
"charge": {
"$ref": "#/definitions/Fee"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
}
}
34 changes: 34 additions & 0 deletions docs/examples/shaping/aliases-firstclass/testdata/refaliases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"Amount": {
"type": "object",
"title": "Amount is the underlying model.",
"properties": {
"cents": {
"description": "Cents is the amount in cents.",
"type": "integer",
"format": "int64",
"x-go-name": "Cents"
},
"currency": {
"description": "Currency is the ISO currency code.",
"type": "string",
"x-go-name": "Currency"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
},
"Fee": {
"description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
"$ref": "#/definitions/Amount"
},
"Receipt": {
"type": "object",
"title": "Receipt references the alias, not the target.",
"properties": {
"charge": {
"$ref": "#/definitions/Fee"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
}
}
48 changes: 48 additions & 0 deletions docs/examples/shaping/aliases-firstclass/testdata/transparent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"Amount": {
"type": "object",
"title": "Amount is the underlying model.",
"properties": {
"cents": {
"description": "Cents is the amount in cents.",
"type": "integer",
"format": "int64",
"x-go-name": "Cents"
},
"currency": {
"description": "Currency is the ISO currency code.",
"type": "string",
"x-go-name": "Currency"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
},
"Fee": {
"description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
"type": "object",
"properties": {
"cents": {
"description": "Cents is the amount in cents.",
"type": "integer",
"format": "int64",
"x-go-name": "Cents"
},
"currency": {
"description": "Currency is the ISO currency code.",
"type": "string",
"x-go-name": "Currency"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
},
"Receipt": {
"type": "object",
"title": "Receipt references the alias, not the target.",
"properties": {
"charge": {
"$ref": "#/definitions/Amount"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
}
}
8 changes: 3 additions & 5 deletions docs/examples/shaping/aliases/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
// how-to. aliases_test.go scans it and writes the golden fragment the guide
// renders.
//
// NOTE: the first-class-alias modes (a swagger:model type alias under the
// default/RefAliases modes) currently hang the scanner — see doc-site-quirks F9.
// This example deliberately uses an unannotated alias (the dissolve case), which
// is the safe, common behavior; the first-class modes are described
// conceptually in the guide until F9 is fixed.
// This example covers the DISSOLVE case (an unannotated alias), which is the
// common behaviour the guide leads with. The first-class-alias modes have their
// own witness next door, in shaping/aliases-firstclass.
package aliases

// snippet:alias
Expand Down
Loading
Loading