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
6 changes: 3 additions & 3 deletions docs/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ Error, for `FCMS1216`. A field property has no public getter or no public setter

### FCMS0007

Error, for `FCMS1216`. `[Select]` on an enum lists options. The members of the enum are the options; remove the list.
Error, for `FCMS1216`. `[Select]` on an enum lists options, or is given `null` for them. The members of the enum are the options; remove the list.

### FCMS0008

Error, for `FCMS1216`. `[Select]` on a property that is not an enum lists no options. List them, as in `[Select("news", "blog")]`.
Error, for `FCMS1216`. `[Select]` on a property that is not an enum lists no options, or is given `null` for them. List them, as in `[Select("news", "blog")]`.

### FCMS0009

Expand All @@ -60,7 +60,7 @@ Error, for `FCMS1210`. A `[Component]` property holds a class, or a list of a cl

### FCMS0011

Error, for `FCMS1211`. `[Blocks]` lists no block types. List them, as in `[Blocks(typeof(HeroBlock))]`.
Error, for `FCMS1211`. `[Blocks]` lists no block types, or is given `null` for them. List them, as in `[Blocks(typeof(HeroBlock))]`.

### FCMS0012

Expand Down
6 changes: 3 additions & 3 deletions docs/planning/backend-technical-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1660,7 +1660,7 @@ Codes `FCMS3350` to `FCMS3359` are the host's and the tool's (an unknown role, s

- TypeScript: one module, `fluentcms.ts`, which imports `@fluentcms/client` as the namespace `fluentcms`, so no name of the schema clashes with an export: an interface per content type (extending `Entry`, with the system fields), component (`SeoComponent`) and block type (`HeroBlock`, with `type` as its literal and an optional `id`); a blocks field is the union of its block types, a relation `Relation<Author, "author">`, the reference or the populated entry, a media field `MediaReference`, rich text `RichText`, a select the union of its options, and a localized type has `fallbacks` with its localizable fields. The builders are those the C# emitter gives, by name, with their members in camel case (`ArticleFields.sections.hero.heading.eq("x")`), and `createClient(options)` binds each content type to a collection or singleton client.
- C#: a class per content type (`[ContentType("article")]`, deriving from `Entry`), component (`SeoComponent`) and block type (`HeroBlock`, deriving from the generated `Block`, which names its derived types for `type` and reads one it does not know as itself), with `JsonPropertyName` on every property; a relation is `Relation<Author>`, a media field `MediaReference`, rich text and JSON `JsonElement`, and a localized type has `{Name}Fallbacks`. The builders are those of the emitter of B-95 over the field types of `FluentCMS.Client`, so that `ArticleFields.Title.Contains("x")` reads the same in a host and in a client. Every file carries the schema hash in its header and is written only when it changed; a file with the header of an earlier run that is no longer generated, such as the class of a removed type, is removed.
- `--check` writes nothing, and exits 1 with `FCMS3372`, naming each file that differs, is missing or is no longer generated, when the files on disk are not what the schema gives. A field type without a generated type is JSON or `unknown` with `FCMS3375`, and a name that gives no identifier or is taken, in any case, gets nothing, with `FCMS3376`; both are warnings and the command exits 0. A class name is a file name, so `foo-bar` and `foobar`, `FooBar` and `Foobar`, would be one file where a file system ignores case: the second gets no type, in C# and in TypeScript alike, and a set of files whose names differ in case alone is refused before anything is written or checked (`FCMS3380`), whatever gave them ([#3070](https://github.com/fluentcms/FluentCMS/issues/3070)). A C# file of the command's that names another namespace is another run's: it is neither removed as no longer generated nor counted so by `--check`, and a run that would write over one is refused with nothing written (`FCMS3381`), so two namespaces need two folders, as the tool's README says. Other codes: `FCMS3371` for a request for the OpenAPI document of an instance that fails (with the status and the problem's code, never the key), `FCMS3373` for files that cannot be written, `FCMS3374` for a namespace that is not one and `FCMS3378` for a host that wrote no snapshot or document. Codes `FCMS3370` to `FCMS3381` of the range `FCMS3370` to `FCMS3389`.
- `--check` writes nothing, and exits 1 with `FCMS3372`, naming each file that differs, is missing or is no longer generated, when the files on disk are not what the schema gives. A field type without a generated type is JSON or `unknown` with `FCMS3375`, and a name that gives no identifier or is taken, in any case, gets nothing, with `FCMS3376`; both are warnings and the command exits 0. A class name is a file name, so `foo-bar` and `foobar`, `FooBar` and `Foobar`, would be one file where a file system ignores case: the second gets no type, in C# and in TypeScript alike, and no builder (`FCMS0100`, 6.4, [#3071](https://github.com/fluentcms/FluentCMS/issues/3071)), and a set of files whose names differ in case alone is refused before anything is written or checked (`FCMS3380`), whatever gave them ([#3070](https://github.com/fluentcms/FluentCMS/issues/3070)). A C# file of the command's that names another namespace is another run's: it is neither removed as no longer generated nor counted so by `--check`, and a run that would write over one is refused with nothing written (`FCMS3381`), so two namespaces need two folders, as the tool's README says. Other codes: `FCMS3371` for a request for the OpenAPI document of an instance that fails (with the status and the problem's code, never the key), `FCMS3373` for files that cannot be written, `FCMS3374` for a namespace that is not one and `FCMS3378` for a host that wrote no snapshot or document. Codes `FCMS3370` to `FCMS3381` of the range `FCMS3370` to `FCMS3389`.
- The clients: `FluentCmsClient` over the caller's `HttpClient`, which carries the key or the bearer token, and `FluentCmsClient` of `@fluentcms/client` over `fetch` with the headers given; an answer that is not a success throws `FluentCmsProblemException` or `FluentCmsProblem` with the status, `code`, `title`, `detail`, `errors` and `traceId` of the problem. A success whose body is not JSON, such as a page of a proxy, throws the same type with the status, no code and the detail `The answer is not JSON.`, and not the exception of a JSON reader, and an empty success, whether it says so by its length or not, answers nothing ([#3070](https://github.com/fluentcms/FluentCMS/issues/3070)). A write leaves out the system fields and sends a relation or a media field as its id, and an update sends `If-Match` with the version read. Both write a query as the bracket grammar of 4.4 in the order and with the escaping of the server's writer, and are held to one list of filters and the query strings they give, `clients/filter-cases.json`, whose cases on the Blog sample's fields a test sends to its Delivery API, checking below it that the content service is given the query each case builds, with its operands as the case names them ([#3070](https://github.com/fluentcms/FluentCMS/issues/3070)), so the query strings both clients are held to read on the server as they were built. A moment is written in UTC with `Z` and no trailing zeros in its fraction, which a JavaScript `Date` and a `DateTimeOffset` both give.

### 6.2 Templates
Expand All @@ -1683,13 +1683,13 @@ The `Templates` job of CI packs every package of the build with the version `1.0

`FluentCMS.Generators` emits, per content type in the host: a typed filter and sort builder (`ArticleFields.Title.Contains("x")`) and block union helpers. The field types the builders return are in the SDK. `FluentCMS.Analyzers` reports `FCMS0xxx` diagnostics: invalid attribute combinations, reserved names, slug source missing, relation to a non-content type, `Localizable` on a non-localized type, a filtered field without `Indexed` (info). Each ships as a package of its own that a host references.

**Source generator as built (B-95).** `FluentCMS.Generators` is an incremental generator that a host references as an analyzer, as the Blog sample and the reference host do. It finds the classes marked `[Collection]`, `[Singleton]`, `[Component]` or `[Block]` and reads them as the attribute reader does (4.2): the same names (`JsonNamingPolicy.CamelCase` for a field, `KebabCaseLower` for a type, ported because the generator runs on netstandard2.0), the inherited properties first, and only the properties with one field attribute and a public getter and setter. A class the reader refuses gets no builder and no word from the generator, since the host reports it at start and the analyzers in the editor. It emits, in the namespace of the class:
**Source generator as built (B-95).** `FluentCMS.Generators` is an incremental generator that a host references as an analyzer, as the Blog sample and the reference host do. It finds the classes marked `[Collection]`, `[Singleton]`, `[Component]` or `[Block]` and reads them as the attribute reader does (4.2): the same names (`JsonNamingPolicy.CamelCase` for a field, `KebabCaseLower` for a type, ported because the generator runs on netstandard2.0), the inherited properties first, and only the properties with one field attribute and a public getter and setter. A class the reader refuses gets no builder and no word from the generator, since the host reports it at start and the analyzers in the editor. Nor does it report a field the reader refuses, such as `[Blocks(null)]`, which the host refuses with `FCMS1211` and the analyzers with `FCMS0011`, as for `[Blocks]` with no types, or `[Select(null)]`, which the host refuses with `FCMS1216` and the analyzers with `FCMS0007` on an enum and `FCMS0008` elsewhere, as for a list on an enum and no options elsewhere. A blocks field it cannot read gets no member, and the other members and classes keep their builders ([#3071](https://github.com/fluentcms/FluentCMS/issues/3071)). It emits, in the namespace of the class:

- `{Type}Fields` for a collection or singleton, a static class with a member per field, so that `ArticleFields.Title.Contains("x")` gives the same `FieldFilter` as `new FieldFilter("title", FilterOperator.Contains, "x")` and `ArticleFields.PublishedOn.Descending()` a `SortField`.
- `{Name}ComponentFields` for a component, whose members carry the path of the field that holds it: `ArticleFields.Seo.MetaTitle` is `seo.metaTitle`, at any depth of single components. A repeated component is JSON a filter cannot reach and gets no member.
- `{Name}BlockFields` for a block type, with a member for each scalar field one level inside the list (Q7), and for each blocks field a nested class over `BlocksField` with the block types of the field and their presence: `ArticleFields.Sections.Hero.Heading.Eq("x")` is `sections.hero.heading`, `ArticleFields.Sections.HasHero()` is `hasBlock` with `hero`.

The class names come from the schema names, not the CLR names, so that a client emits the same code from a snapshot; the suffixes keep a component or a block from taking the name of a collection, and a name that still clashes, such as a collection `x-block` beside a block `x`, gives the builder to the first and reports the other with `FCMS0100`. A block type whose name has a hyphen gets no builder (`FCMS0104`), because a segment of a filter path holds only letters, digits and underscores; its block lists still have `Has{Name}()`. The members are named from the field names. The builders are public, so a library that declares content types shares its builders with the host.
The class names come from the schema names, not the CLR names, so that a client emits the same code from a snapshot; the suffixes keep a component or a block from taking the name of a collection, and a name that still clashes, such as a collection `x-block` beside a block `x`, gives the builder to the first and reports the other with `FCMS0100`. Class names are compared ignoring case, as Roslyn compares the names of sources, each named after its class, and as `generate` compares its files: `x-y` and `xy` would give `XYFields` and `XyFields`, and only `x-y` gets a builder ([#3071](https://github.com/fluentcms/FluentCMS/issues/3071)). A block type whose name has a hyphen gets no builder (`FCMS0104`), because a segment of a filter path holds only letters, digits and underscores; its block lists still have `Has{Name}()`. It still takes its class name, as it takes its class in `generate`, so that the class and the builder of a name belong to one type: beside the block type `a-b`, `ab` gets no builder (`FCMS0100`), as it gets no class (`FCMS3376`) ([#3071](https://github.com/fluentcms/FluentCMS/issues/3071)). The members are named from the field names. The builders are public, so a library that declares content types shares its builders with the host.

The field types are in the SDK, in `FluentCMS.Content`, each a `QueryField` with its path and only the operators the engine accepts for its kind: `TextField` (`text`, `slug`, `email`, `url`), `LongTextField`, `RichTextField`, `NumberField<T>` (`long` for `integer`, `decimal`), `DateField<T>` (`DateOnly` for `date`, `DateTimeOffset` for `dateTime`), `BooleanField`, `SelectField` and `SelectListField` for a single and a multiple select, `RelationField` and `MediaField` by id, `JsonField` with `Key(name)` giving a `JsonKeyField`, `BlocksField`, and inside a block `BlockTextField`, `BlockValueField<T>` and `BlockContainsField`. A field that the engine can sort has `Ascending()` and `Descending()`. A select takes the option as the API shows it (`video`), not the enum member. System fields such as `createdAt` have no builder yet.

Expand Down
15 changes: 6 additions & 9 deletions src/FluentCMS.Analyzers/SchemaReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,22 +272,18 @@ private static bool ReadFieldType(SchemaField field, string location, List<Findi
var attribute = field.FieldAttribute;
var (element, multiple) = Unwrap(property.Type);

// [Select(null)] or [Blocks(null)] stops the reader with an exception rather than a code, so nothing is reported.
if (field.FieldType is "select" or "blocks" && attribute.ConstructorArguments is { Length: 1 } arguments && arguments[0].IsNull)
{
return false;
}

switch (field.FieldType)
{
case "decimal":
case "slug" when Named(attribute, "Source") is string:
field.SlugSource = Named(attribute, "Source") as string;
return true;

// As the reader does, [Select(null)] is refused as a list on an enum and as no options elsewhere.
case "select":
var options = Array(attribute).Length;
if (element.TypeKind == TypeKind.Enum && options > 0)
var isNull = attribute.ConstructorArguments is { Length: 1 } arguments && arguments[0].IsNull;
if (element.TypeKind == TypeKind.Enum && (options > 0 || isNull))
{
findings?.Add(new Finding(Rules.EnumWithOptions, LocationOf(attribute), [location]));
return false;
Expand Down Expand Up @@ -385,9 +381,10 @@ private static bool ReadRelation(SchemaField field, string location, List<Findin
return true;
}

// The values of a params array, such as the options of [Select] or the block types of [Blocks].
// The values of a params array, such as the options of [Select] or the block types of [Blocks]; none for a null array,
// whose Values throw when used.
private static ImmutableArray<TypedConstant> Array(AttributeData attribute) =>
attribute.ConstructorArguments.Length == 1 && attribute.ConstructorArguments[0].Kind == TypedConstantKind.Array
attribute.ConstructorArguments.Length == 1 && attribute.ConstructorArguments[0] is { Kind: TypedConstantKind.Array, IsNull: false }
? attribute.ConstructorArguments[0].Values
: ImmutableArray<TypedConstant>.Empty;

Expand Down
2 changes: 1 addition & 1 deletion src/FluentCMS.Cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ dotnet fluentcms migrations add Initial
dotnet fluentcms migrate
```

`generate csharp --namespace <name> --output <folder>` writes a file for each class into the folder and removes the files it wrote before and no longer generates. A folder holds the code of one namespace: a run with another `--namespace` neither removes nor writes over the files of the first, and is refused with `FCMS3381` when it would write one of them. Generate each namespace into a folder of its own, and to change the namespace of a folder, remove its generated files first. Two classes whose names differ in case alone would be one file where the file system ignores case, so the second type gets no class (`FCMS3376`), and files whose names differ in case alone are never written (`FCMS3380`).
`generate csharp --namespace <name> --output <folder>` writes a file for each class into the folder and removes the files it wrote before and no longer generates. A folder holds the code of one namespace: a run with another `--namespace` neither removes nor writes over the files of the first, and is refused with `FCMS3381` when it would write one of them. Generate each namespace into a folder of its own, and to change the namespace of a folder, remove its generated files first. Two classes whose names differ in case alone would be one file where the file system ignores case, so the second type gets no class (`FCMS3376`) and no builder (`FCMS0100`), and files whose names differ in case alone are never written (`FCMS3380`).

[Run with Docker](https://github.com/fluentcms/FluentCMS/blob/main/docs/docker.md#administer-an-instance) lists the commands, which the image runs too.
8 changes: 5 additions & 3 deletions src/FluentCMS.Core/Schema/AttributeSchemaReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,17 @@ private void ReadFieldType(FieldAttribute attribute, PropertyInfo property, stri
steps.Add(f => f.Setting(FieldSettings.Source, SchemaNaming.ToCamelCase(source)));
break;

// [Select(null)] gives no list at all, which is refused as a list on an enum and as no options elsewhere.
case SelectAttribute select:
if (element.IsEnum && select.Options.Count > 0)
if (element.IsEnum && select.Options is not { Count: 0 })
{
fail(SchemaErrorCodes.InvalidProperty, $"property {location} is an enum, so its members are the options and [Select] must not list any");
}
else if (element.IsEnum)
{
steps.Add(f => f.Setting(FieldSettings.Options, Enum.GetNames(element).Select(SchemaNaming.ToCamelCase).ToArray()));
}
else if (select.Options.Count > 0)
else if (select.Options is { Count: > 0 })
{
steps.Add(f => f.Setting(FieldSettings.Options, select.Options));
}
Expand Down Expand Up @@ -350,7 +351,8 @@ private static void ReadRelation(RelationAttribute relation, PropertyInfo proper

private void ReadBlocks(BlocksAttribute blocks, string location, List<Action<IFieldBuilder>> steps, Action<string, string> fail)
{
if (blocks.BlockTypes.Count == 0)
// [Blocks(null)] gives no list at all.
if (blocks.BlockTypes is not { Count: > 0 })
{
fail(SchemaErrorCodes.InvalidField, $"blocks field {location} must list the block types it accepts");
return;
Expand Down
Loading
Loading