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
47 changes: 47 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Repository Guidelines

## Project Structure & Module Organization
- Core: `generate.go`, `types.go`, `inline_refs.go`, `genstate.go`.
- DSL and expressions: `dsl/`, `expr/`.
- Example: `examples/calc/` with generated code in `gen/` and binaries in `cmd/`.
- Tests and goldens: `*_test.go` and `testdata/*.json`.
- Docs: `README.md`, `INLINE_REFS.md`.

## Build, Test, and Development Commands
- `make all`: run gen, tests, lint, build examples, then clean (via `../plugins.mk`).
- `make lint`: `goimports` formatting check and `staticcheck` lint.
- `make test`: run `go test ./...`; verbose: `go test -v ./...`.
- `make gen`: regenerate example outputs (includes `examples/calc/gen/docs.json`).
- Update goldens: `go test ./... -- -update` (commit changes in `testdata/`).

## Coding Style & Naming Conventions
- Follow Goa’s CLAUDE.md layout: group declarations in this order per file — types, consts, vars, public funcs, public methods, private funcs, private methods. No section markers.
- Keep files focused and reasonably small; one main construct per file.
- Prefer `any` over `interface{}` in new code; exported identifiers use CamelCase; packages are short, lower-case.
- Never edit generated code in `examples/calc/gen/`; fix generators/templates instead.

## Curly Braces Rules
- Default: use multi-line braces for all code blocks (Go and Goa DSL).
- Exceptions only:
- Empty DSL closures may be single-line, e.g., `JSONRPC(func() {})`.
- Trivial methods returning a constant may be single-line, e.g., `func (e *Enum) String() string { return "foo" }`.
- Do not compress control flow. Preferred:

```go
if err != nil {
return err
}
```

Avoid: `if err != nil { return err }`.
- Place `else` on the same line as the closing `}` of the preceding block.

## Testing Guidelines
- Use `testing` plus `testify/assert` and `testify/require`.
- Golden tests compare generated output to files in `testdata/` (e.g., `no-payload-no-return.json`).
- When behavior changes intentionally, regenerate goldens and review diffs carefully.

## Commit & Pull Request Guidelines
- Conventional commits: `feat(docs): ...`, `fix(docs): ...`, `chore: ...`.
- PRs include description, rationale, linked issues, and testing notes; keep changes small and scoped.
- If generation output changes, run `make gen` and commit relevant updates under `examples/calc/gen/` and `testdata/`.
138 changes: 138 additions & 0 deletions docs/INLINE_REFS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
## Goal

Design a robust, deterministic strategy to inline all JSON Schema $ref occurrences into their target schemas for docs JSON produced by goa. The generator only uses local references to the top-level definitions object (paths of the form "#/definitions/<Name>").

## Where $ref appear in goa’s output

- **User/Result types**: Type schemas reference definitions via `TypeRef*` and `ResultTypeRef*`.
- **Object properties**: Nested fields can be `$ref`.
- **Array items**: `items` may be a `$ref` when the element type is a user/result type.
- **Map values**: `additionalProperties` may be a schema containing `$ref` (or boolean `true`).
- **Union types**: `anyOf` is an array of schemas that may contain `$ref`.
- **Service-level top schema**: APISchema properties refer to `#/definitions/<Service>`.

Assumption: Only references to `#/definitions/<Name>` are present. No external files/URLs.

## Constraints

- Definitions are in a per-root map; keys are type names.
- Definitions can themselves contain nested `$ref`.
- Must avoid infinite recursion on cyclical definitions.
- Preserve required fields, examples, and order where applicable.

## Strategy (high-level)

1) **Snapshot definitions**
- Work on a deep-copied map of the per-root `definitions` to avoid mutating shared state.

2) **Inlining pass**
- Implement `inlineRefs(schema, defs, stack)`:
- If `schema.Ref == "#/definitions/<Name>"`:
- If `<Name>` is in `stack`, keep `$ref` (cycle break) and return.
- Else: push `<Name>`, deep-copy `defs[Name]`, recursively `inlineRefs` on the copy, then replace current node with the copy and clear `Ref`. Pop `<Name>`.
- If `schema.Ref == ""`: recursively process all composite positions:
- `properties` values
- `items`
- `additionalProperties` when it is a `*Schema`
- `anyOf` elements
- (If present) nested `definitions` for completeness

3) **Apply order**
- Perform schema transforms that affect keys first (e.g., JSON tag rename and required filtering), then inline.

4) **Service-level application**
- Run `inlineRefs` on every reachable schema under services (payload, result, streaming payload/result, error types).
- Optionally inline inside `definitions` if you plan to drop them entirely.

5) **Cycles**
- Use a `stack` (set of definition names being expanded). If a name is already in the stack, retain `$ref` at that edge to prevent infinite expansion. This yields minimal `$ref` for strongly-cyclic graphs.

6) **AnyOf**
- Goa builds `anyOf` by appending union variants in-order. Inline each element; preserve order.

7) **Maps**
- If `additionalProperties` is `true`, leave as is. If it is a schema, inline there as well.

8) **Performance**
- Start without memoization. If profiling reveals hotspots, consider caching fully inlined, deep-copied definitions by name and reusing them, taking care not to share mutable pointers unexpectedly.

9) **Post-processing**
- If you require a completely `$ref`-free document, remove `definitions` after inlining all reachable schemas. Otherwise, keep or prune unused definitions based on tests/goldens.

## Pseudocode

```go
func inlineAllServiceSchemas(d *data, defs map[string]*openapi.Schema) {
stack := map[string]bool{}
for _, s := range d.Services {
for _, m := range s.Methods {
if m.Payload != nil && m.Payload.Type != nil {
inlineRefs(m.Payload.Type, defs, stack)
}
if m.StreamingPayload != nil && m.StreamingPayload.Type != nil {
inlineRefs(m.StreamingPayload.Type, defs, stack)
}
if m.Result != nil && m.Result.Type != nil {
inlineRefs(m.Result.Type, defs, stack)
}
if m.StreamingResult != nil && m.StreamingResult.Type != nil {
inlineRefs(m.StreamingResult.Type, defs, stack)
}
for _, e := range m.Errors {
if e.Type != nil {
inlineRefs(e.Type, defs, stack)
}
}
}
}
}

func inlineRefs(s *openapi.Schema, defs map[string]*openapi.Schema, stack map[string]bool) {
if s == nil {
return
}
if s.Ref != "" {
const prefix = "#/definitions/"
if !strings.HasPrefix(s.Ref, prefix) { return }
name := strings.TrimPrefix(s.Ref, prefix)
if stack[name] { return }
def, ok := defs[name]
if !ok || def == nil { return }
stack[name] = true
copy := dupSchema(def)
inlineRefs(copy, defs, stack)
*s = *copy
s.Ref = ""
delete(stack, name)
return
}
for _, p := range s.Properties { inlineRefs(p, defs, stack) }
if s.Items != nil { inlineRefs(s.Items, defs, stack) }
if ap, ok := s.AdditionalProperties.(*openapi.Schema); ok && ap != nil {
inlineRefs(ap, defs, stack)
}
for _, a := range s.AnyOf { inlineRefs(a, defs, stack) }
for _, d := range s.Definitions { inlineRefs(d, defs, stack) }
}
```

## Integration points in the plugin

- After building `docs` and `defs`, and after JSON tag transforms:
- Gate behind `InlineRefs` option: call `inlineAllServiceSchemas(docs, defs)`.
- Decide whether to keep or drop `docs.Definitions` based on goldens. If keeping, you may optionally prune unused definitions.

## Edge cases and guarantees

- **Cycles**: Minimal `$ref` retained on cycle entry.
- **Required and examples**: Preserved; JSON-tag transform already remaps them pre-inlining.
- **Maps**: Free-form (`true`) untouched; schema-valued inlined.
- **Unions**: Order preserved in `anyOf`.

## Rationale

- Tailored to the shapes produced by goa: only `#/definitions/` refs.
- Deterministic and safe (deep copies, cycle guard), compositional (handles all container fields).
- Clean pipeline: rename/tag first, inline second.


Loading
Loading