diff --git a/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs b/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs index 26107525..757ada92 100644 --- a/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs +++ b/crates/macros/cgp-macro-core/src/types/delegate_component/entries.rs @@ -10,8 +10,9 @@ use crate::types::delegate_component::{ /// A table body: leading statements (`open`/`namespace`/`for`) followed by the /// comma-separated mappings. Statements must lead, which is why one written -/// after a mapping fails to parse. -#[derive(Debug, Clone)] +/// after a mapping fails to parse. The `Default` value is the empty table, which +/// is what a `cgp_namespace!` header written without a body parses to. +#[derive(Debug, Clone, Default)] pub struct DelegateEntries { pub statements: Vec, pub entries: Punctuated, diff --git a/crates/macros/cgp-macro-core/src/types/namespace/table.rs b/crates/macros/cgp-macro-core/src/types/namespace/table.rs index 8834afc9..a06d0832 100644 --- a/crates/macros/cgp-macro-core/src/types/namespace/table.rs +++ b/crates/macros/cgp-macro-core/src/types/namespace/table.rs @@ -37,7 +37,9 @@ impl Parse for NamespaceTable { None }; - let entries = { + let entries = if input.is_empty() { + Default::default() + } else { let body; braced!(body in input); diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs index 00c9e517..d10891f5 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/cgp_namespace.rs @@ -4,6 +4,10 @@ //! discarding it. It rejects an attribute on a `:` mapping key, on a `=>` redirect //! key, and on a key inside a `for <..> in ..` loop. //! +//! The header may stand alone: `new Ns` or `new Child: Parent` with no brace pair +//! parses as an empty table. That relaxation applies only to an input that ends +//! at the header, so any other token in the body position is still rejected. +//! //! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_namespace.md (Tests) for these failure //! cases, and cgp-knowledge-base/cgp/reference/macros/cgp_namespace.md for the user-facing //! semantics. @@ -52,3 +56,25 @@ fn rejects_attribute_on_for_loop_key() { )) }); } + +#[test] +fn rejects_stray_token_after_parent() { + // A header-only namespace is accepted only when nothing follows the header. + // A `;` where the body would go is neither a table nor the end of input. + assert_macro_rejects("cgp_namespace with a `;` after the parent", || { + cgp_macro_lib::cgp_namespace(quote!( + new MyNamespace: DefaultNamespace; + )) + }); +} + +#[test] +fn rejects_stray_token_after_name() { + // The same without a parent: a bare identifier after the namespace name is + // not a table, so the `braced!` parse still fails. + assert_macro_rejects("cgp_namespace with a stray token after the name", || { + cgp_macro_lib::cgp_namespace(quote!( + new MyNamespace foo + )) + }); +} diff --git a/crates/tests/cgp-tests/tests/namespaces/mod.rs b/crates/tests/cgp-tests/tests/namespaces/mod.rs index c7ede1c9..5b5fdc8f 100644 --- a/crates/tests/cgp-tests/tests/namespaces/mod.rs +++ b/crates/tests/cgp-tests/tests/namespaces/mod.rs @@ -41,3 +41,7 @@ pub mod default_impls_wiring; pub mod extended; pub mod extended_namespace_wiring; pub mod for_where_clause; + +// A header with no table: `new Ns` and `new Child: Parent` written without a +// brace pair, pinned as snapshots and consumed through a joining context. +pub mod namespace_header_only; diff --git a/crates/tests/cgp-tests/tests/namespaces/namespace_header_only.rs b/crates/tests/cgp-tests/tests/namespaces/namespace_header_only.rs new file mode 100644 index 00000000..5f28f1c1 --- /dev/null +++ b/crates/tests/cgp-tests/tests/namespaces/namespace_header_only.rs @@ -0,0 +1,75 @@ +//! A `cgp_namespace!` that ends at its header, with no brace pair after it. +//! +//! When a namespace has no entries of its own the body may be omitted: +//! `cgp_namespace! { new HeaderOnlyNamespace }` emits just the marker struct and +//! the lookup trait, and `cgp_namespace! { new HeaderOnlyExtendedNamespace: DefaultNamespace }` +//! adds the inheritance impl, exactly as the same headers followed by `{ }` do. +//! Both snapshots belong to this concept, which owns `cgp_namespace!`. `App` then +//! joins the header-only inheriting namespace and wires the inherited error type +//! and raiser components under their full `@cgp.core.error.*` paths, and the +//! `CheckApp` bundle asserts each wired capability resolves, confirming the +//! braceless definition is a usable namespace and not just a parse. That +//! `delegate_components!` is written plainly, since its `namespace` form is +//! pinned in `prefix_default_namespace`. +//! +//! See cgp-knowledge-base/cgp/implementation/entrypoints/cgp_namespace.md and +//! cgp-knowledge-base/cgp/reference/macros/cgp_namespace.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent}; +use cgp::extra::error::RaiseFrom; +use cgp::prelude::*; +use cgp_macro_test_util::snapshot_cgp_namespace; + +snapshot_cgp_namespace! { + cgp_namespace! { + new HeaderOnlyNamespace + } + + expand_header_only_namespace(output) { + insta::assert_snapshot!(output, @" + pub struct __HeaderOnlyNamespaceComponents; + pub trait HeaderOnlyNamespace<__Table__> { + type Delegate; + } + ") + } +} + +snapshot_cgp_namespace! { + cgp_namespace! { + new HeaderOnlyExtendedNamespace: DefaultNamespace + } + + expand_header_only_extended_namespace(output) { + insta::assert_snapshot!(output, @" + pub struct __HeaderOnlyExtendedNamespaceComponents; + pub trait HeaderOnlyExtendedNamespace<__Table__> { + type Delegate; + } + impl<__Table__, __Key__, __Value__> HeaderOnlyExtendedNamespace<__Table__> for __Key__ + where + __Key__: DefaultNamespace<__HeaderOnlyExtendedNamespaceComponents>, + __Key__: DefaultNamespace<__Table__, Delegate = __Value__>, + { + type Delegate = __Value__; + } + ") + } +} + +pub struct App; + +delegate_components! { + App { + namespace HeaderOnlyExtendedNamespace; + + @cgp.core.error.ErrorTypeProviderComponent: + UseType, + @cgp.core.error.ErrorRaiserComponent.{&'static str, String}: + RaiseFrom, + } +} + +pub trait CheckApp: HasErrorType + CanRaiseError<&'static str> + CanRaiseError {} + +impl CheckApp for App {} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 10001973..99e619bb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.96.1" +channel = "1.98.1" profile = "default"