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
Original file line number Diff line number Diff line change
Expand Up @@ -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<DelegateStatement>,
pub entries: Punctuated<DelegateMapping, Comma>,
Expand Down
4 changes: 3 additions & 1 deletion crates/macros/cgp-macro-core/src/types/namespace/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
))
});
}
4 changes: 4 additions & 0 deletions crates/tests/cgp-tests/tests/namespaces/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
75 changes: 75 additions & 0 deletions crates/tests/cgp-tests/tests/namespaces/namespace_header_only.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
@cgp.core.error.ErrorRaiserComponent.{&'static str, String}:
RaiseFrom,
}
}

pub trait CheckApp: HasErrorType + CanRaiseError<&'static str> + CanRaiseError<String> {}

impl CheckApp for App {}
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.96.1"
channel = "1.98.1"
profile = "default"
Loading