# ContentSettings



`ContentSettings` is the smallest yet most load-bearing class in the
DSL. It's the **only** thing connecting a Projection instance to its
identifier (name + entity type), its export path, and its enrichments.
Drivers compute it before construction; Projections consume it during
`toString()`.

## Source [#source]

`skmtc/deno/core/dsl/ContentSettings.ts`

## Constructor [#constructor]

```ts
class ContentSettings<E = undefined> {
  identifier: IdentifierBase
  exportPath: string
  enrichments: E
  variant: string

  constructor(args: {
    identifier: IdentifierBase
    exportPath: string
    enrichments: E
    variant: string
  })

  static empty(args: {
    identifier: IdentifierBase
    exportPath: string
    variant?: string            // defaults to 'main'
  }): ContentSettings<undefined>
}
```

```ts
new ContentSettings({
  identifier: IdentifierBase,    // the assembled identifier (name + lang-typed parts)
  exportPath: string,            // the file path this Projection lands in
  enrichments: E,                // the validated enrichment payload (or undefined)
  variant: string                // 'main' for variants-unaware Projections
})
```

The generic parameter `E` is the enrichment shape — declared by the
generator's `toEnrichmentSchema` factory. When the generator declares
no enrichments, `E = undefined`.

## Properties [#properties]

### `identifier` [#identifier]

The Projection's name and entity-type marker. Used in two ways:

1. **In the rendered output** — wrapped by `Definition`, this becomes
   the `export const NAME =` or `export type Name =` declaration.
2. **In the cross-generator coordination cache** — the
   `(identifier.name, exportPath)` pair is the cache key. Two
   generators producing the same name in the same file converge on
   one entry.

See [API: Identifier](/docs/reference/api/dsl-identifier) for the entity-type
semantics (`TsEntityType`:
`'variable' | 'type' | 'class' | 'interface' | 'namespace'`; the
rendered declaration keyword for `'variable'` is `const`) and the
factory functions.

### `exportPath` [#exportpath]

The file path where this Projection's `Definition` will live in the
output. Computed by the Projection's static `toExportPath()` method,
typically:

```ts
static toExportPath({ operation }): string {
  return `${basePath}/forms/${toEndpointName(operation)}.tsx`
}
```

The path is the second half of the cache key — two Projections with
the same `identifier.name` but different `exportPath` are *distinct*
entries (no collision).

The Driver also uses `exportPath` to route the Projection's
`register({ definitions })` call to the right `File`.

### `enrichments` [#enrichments]

The validated enrichment payload for this specific Projection
instance. The projection-base factory routes via a key path that
depends on the factory kind (OAS operation, model, or GraphQL
operation) — see
[enrichments-shape reference](/docs/reference/settings/enrichments-shape)
for the three routing structures.

The result is type-narrowed to `E`, the schema declared by the
generator's `toEnrichmentSchema` factory. May be `undefined` when:

* The generator declared no enrichments (`E = undefined`)
* The user didn't supply enrichments for this operation
* The enrichments are optional and absent

The Projection's `toString()` reads `this.settings.enrichments?.X` to
access user-supplied overrides.

## Factory [#factory]

### `ContentSettings.empty(args)` [#contentsettingsemptyargs]

A convenience factory for the no-enrichments case. Equivalent to:

```ts
new ContentSettings({
  identifier,
  exportPath,
  enrichments: undefined
})
```

Used by Drivers when constructing settings for generators that don't
declare an enrichment schema. The static method makes the intent
explicit: "no enrichments here, just the identifier and path."

```ts
const settings = ContentSettings.empty({
  identifier: createVariable('userBody'),
  exportPath: '@/types/userBody.generated.ts'
})
// → ContentSettings<undefined>
```

## Use sites [#use-sites]

### In Projection constructors (via `this.settings`) [#in-projection-constructors-via-thissettings]

The Driver constructs `ContentSettings` and passes it to the
Projection constructor:

```ts
const settings = new ContentSettings({
  identifier: lang.toIdentifier({
    name: projection.toIdentifierName({ operation, enrichments, variant }),
    ...projection.toIdentifierType(operation, context)
  }),
  exportPath: projection.toExportPath({ operation, enrichments, variant }),
  enrichments,
  variant
})

const instance = new projection({
  context,
  operation,
  settings              // ← passed in
})
```

Inside the Projection, the settings are available as `this.settings`:

```ts
// ShadcnFormBase = toOasOperationProjectionBase({...}) in base.ts

class ShadcnForm extends ShadcnFormBase {
  override toString(): string {
    const { title, submitLabel } = this.settings.enrichments.subject ?? {}

    return `
      <Form>
        ${title ? `<h2>${title}</h2>` : ''}
        ${this.fields}
        <Button>${submitLabel ?? 'Submit'}</Button>
      </Form>
    `
  }
}
```

The identifier is used implicitly when the Projection's `Definition`
is rendered (`export const ${this.settings.identifier.name} = ...`).

### In Drivers (computed from projection static methods) [#in-drivers-computed-from-projection-static-methods]

Drivers (`OasOperationDriver`, `OasModelDriver`, `GqlOperationDriver`)
compute `ContentSettings` by calling the projection's static methods.
The flow:

```ts
// In OasOperationDriver (conceptually)
const enrichments = projection.toEnrichments({ operation, context, variant })
const name = projection.toIdentifierName({ operation, enrichments, variant })
const identifier = lang.toIdentifier({
  name,
  ...projection.toIdentifierType(operation, context)
})
const exportPath = projection.toExportPath({ operation, enrichments, variant })

const settings = new ContentSettings({ identifier, exportPath, enrichments, variant })

const instance = new projection({ context, operation, settings })
```

`toIdentifierName` and `toExportPath` are pure functions of
`(operation, enrichments, variant)`. Two calls with the same inputs
produce the same name and path — which is why the cache key
`(identifier.name, exportPath)` is stable across generator
iterations. (`toIdentifierType` is context-aware and runs only on
cache-miss.)

### In the cache key [#in-the-cache-key]

`GenerateContext.findDefinition({ name, exportPath })` searches by
those two fields exactly. They're derived from
`ContentSettings.identifier.name` and `ContentSettings.exportPath`,
respectively. The full `ContentSettings` instance isn't part of the
key — just the two scalars.

Why not the full object? Identity stability. The static methods
recompute fresh objects on every call (they're pure), but their
outputs are equal-by-content. Keying by scalars sidesteps the issue
of comparing object references.

## Examples [#examples]

### Empty (no enrichments) [#empty-no-enrichments]

```ts
const settings = ContentSettings.empty({
  identifier: createVariable('userBody'),
  exportPath: '@/types/userBody.generated.ts'
})

// In the Projection
// UserBodyBase = toModelProjectionBase({...}) in base.ts

class UserBody extends UserBodyBase {
  override toString(): string {
    // No enrichments available; type is undefined
    return `z.object({ name: z.string() })`
  }
}
```

### With enrichments [#with-enrichments]

```ts
type EnrichmentSchema = {
  title?: string
  submitLabel?: string
}

const settings = new ContentSettings<EnrichmentSchema>({
  identifier: createVariable('createUserForm'),
  exportPath: '/forms/CreateUser.generated.tsx',
  enrichments: { title: 'Create User', submitLabel: 'Create' }
})

// ShadcnFormBase = toOasOperationProjectionBase<EnrichmentSchema>({...}) in base.ts

class ShadcnForm extends ShadcnFormBase {
  override toString(): string {
    const { title, submitLabel } = this.settings.enrichments.subject ?? {}
    return `<Form><h2>${title}</h2>...<Button>${submitLabel}</Button></Form>`
  }
}
```

## Common questions [#common-questions]

### Why is `enrichments` inside settings rather than a separate constructor arg? [#why-is-enrichments-inside-settings-rather-than-a-separate-constructor-arg]

Settings is the *bundle*: identifier, exportPath, and enrichments all
flow together from the Driver. Splitting them would require every
Projection constructor to accept three args instead of one — and
would make it harder for projection-base classes to forward them.

The single `settings` object is the consistent interface across
operation, model, and GQL projection bases.

### Why does `ContentSettings.empty` exist if I can just pass `enrichments: undefined`? [#why-does-contentsettingsempty-exist-if-i-can-just-pass-enrichments-undefined]

Two reasons:

1. **Intent clarity** — `ContentSettings.empty({...})` reads as "no
   enrichments here," while `new ContentSettings({..., enrichments: undefined})`
   looks like an oversight.
2. **Type correctness** — `ContentSettings.empty` returns
   `ContentSettings<undefined>` directly, so downstream code sees the
   precise type rather than `ContentSettings<E>` where `E` could be
   anything.

Functionally they're equivalent; semantically the factory is
preferable.

### Can I mutate `settings` after construction? [#can-i-mutate-settings-after-construction]

Don't. `ContentSettings` is treated as immutable by the engine.
Mutating `this.settings.identifier.name` after construction would
desync the cache key from the actual rendered name, leading to
duplicate or missing definitions.

If you need a different identifier for some reason, construct a
fresh `ContentSettings` and a fresh `Definition`. Don't reach into
the existing object.

### What if my generator needs additional per-instance data beyond `ContentSettings`? [#what-if-my-generator-needs-additional-per-instance-data-beyond-contentsettings]

Pass it via the Projection's constructor `args` object:

```ts
// MyBase = toOasOperationProjectionBase({...}) in base.ts

class MyProjection extends MyBase {
  constructor(args: {
    context, operation, settings,
    customData: { ... }       // ← additional field
  }) {
    super(args)
    this.customData = args.customData
  }
}
```

Then read it as `this.customData` in `toString()`. This is the
escape hatch for data that doesn't fit the identifier/path/enrichments
shape.

### Where does `ContentSettings` not flow? [#where-does-contentsettings-not-flow]

Snippets. Snippets don't have a `settings` object — they're anonymous
helpers, not addressable units. Snippets receive their parameters via
their constructor's `args` and have no identifier or exportPath of
their own.

If a Snippet needs enrichment data, the parent Projection extracts it
from `this.settings.enrichments` and passes it to the Snippet
constructor.

## Related types [#related-types]

```ts
class ContentSettings<E = undefined> {
  identifier: IdentifierBase
  exportPath: string
  enrichments: E
  variant: string
}
```

## See also [#see-also]

* [API: Identifier](/docs/reference/api/dsl-identifier) — what `settings.identifier` is
* [API: Projection bases](/docs/reference/api/projection-bases) — how settings are constructed and forwarded
* [API: GenerateContext](/docs/reference/api/generate-context) — `findDefinition({ name, exportPath })` uses the settings' values
* [Reference: enrichments shape](/docs/reference/settings/enrichments-shape) — how `settings.enrichments` is routed
* [Cross-generator coordination concept](/docs/concepts/cross-generator-coordination) — why settings drive the cache key
* [Projections and Snippets concept](/docs/concepts/projections-and-snippets) — settings' role in Projections
* [Glossary: ContentSettings, Identifier, Enrichments](/docs/reference/glossary)
