//! The Rust-owned schema bundle the language SDKs are generated from, and the
//! per-language rendering of one registered document.
//!
//! Keeping every shared contract root here gives every SDK one generation
//! source: the capability manifest, the envelope and filter roots, the schema
//! id, the registry document, each verb's option contract, and every message
//! the registry holds. Language packages may add client behaviour, but must
//! not restate these wire shapes by hand.
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::num::NonZeroU64;
use schemars::{schema_for, JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::ask::{Address, Correlation};
use crate::capability::{Capability, CAPABILITIES};
use crate::carry::CarriedEntry;
use crate::codec::CodecName;
use crate::config::Config;
use crate::envelope::Envelope;
use crate::filter::{Filter, Matcher};
use crate::kinds::KindEntry;
use crate::layout::LayoutDocument;
use crate::link::{BundleVersion, CachedBundle, SchemaBundle, SchemaLink};
use crate::queue::{Asker, QueueStatus};
use crate::schema::{Registry, SchemaId};
use crate::transport::{ConsumerName, Position, QueueName};
use crate::validate::Verdict;
use crate::vocabulary::{Reserved, Vocabulary};
/// How a reading verb renders what it read.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Format {
/// One JSON document (or one per line), the machine-readable contract.
#[default]
Json,
/// A deterministic rendering of the same content for a person.
Text,
}
/// The language `schema gen` renders a document for.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Lang {
/// The registered JSON Schema document, byte for byte.
Json,
/// A Rust declaration that regenerates the document through schemars.
Rust,
/// A Python declaration — generated by the SDK package, not this build.
Python,
/// A TypeScript declaration — generated by the SDK package, not this build.
Typescript,
}
impl Lang {
/// The word `--lang` takes for this language.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Lang::Json => "json",
Lang::Rust => "rust",
Lang::Python => "python",
Lang::Typescript => "typescript",
}
}
}
/// The options of `schema list`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaListOptions {
/// The registry directory, or the `ONEMESSAGEBUS_REGISTRY` environment
/// variable when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
/// How the list is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
/// A configuration whose `schemas` links are resolved and registered beside
/// the registry; not read from `ONEMESSAGEBUS_CONFIG`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
}
/// The options of `schema check`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaCheckOptions {
/// The id to check against.
pub id: SchemaId,
/// The payload file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The registry directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
/// A configuration whose `schemas` links are resolved and registered beside
/// the registry; not read from `ONEMESSAGEBUS_CONFIG`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
}
/// The options of `schema gen`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaGenOptions {
/// The id to render.
pub id: SchemaId,
/// The language to render it for.
pub lang: Lang,
/// The registry directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
/// A configuration whose `schemas` links are resolved and registered beside
/// the registry; not read from `ONEMESSAGEBUS_CONFIG`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
}
/// The options of `schema register`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaRegisterOptions {
/// The id to register under.
pub id: SchemaId,
/// The file holding the JSON Schema document.
pub file: String,
/// The registry directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
/// A configuration whose `schemas` links are resolved and registered beside
/// the registry; not read from `ONEMESSAGEBUS_CONFIG`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
}
/// The options of `events merge`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventsMergeOptions {
/// The stream files to merge.
pub files: Vec<String>,
/// A filter spec: inline JSON, or a path to a YAML document.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
/// The profile whose vocabulary the streams are read through.
#[serde(default, skip_serializing_if = "Option::is_none")]
// llmlint: ignore[invalid_states_unrepresentable] the core names no agent word and knows no profile: the binary in crates/onemessagebus-cli links the profiles and owns their names, and the SDK node reads the set from the manifest, so a closed type here would put the profile's words in the core.
pub profile: Option<String>,
/// How the merged stream is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `events emit`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventsEmitOptions {
/// The stream file to append to.
pub path: String,
/// The kind of the event.
// llmlint: ignore[invalid_states_unrepresentable] these options are the argv an SDK client hands `events emit`, which refuses a kind that is not kebab-case by name at that boundary; `Kind` is an open newtype by contract, because a relay carries a sibling's kinds, so there is no closed kind type in the core to use here.
pub kind: String,
/// The stream id to stamp.
pub stream: String,
/// The source word; the profile's default when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
// llmlint: ignore[invalid_states_unrepresentable] the source words are the chosen profile's, and the core knows no profile: the binary refuses a word outside the profile it runs by name, and a closed type here would put a vocabulary's words in the core.
pub source: Option<String>,
/// The profile whose vocabulary the envelope is written over.
#[serde(default, skip_serializing_if = "Option::is_none")]
// llmlint: ignore[invalid_states_unrepresentable] the core names no agent word and knows no profile: the binary in crates/onemessagebus-cli links the profiles and owns their names, and the SDK node reads the set from the manifest, so a closed type here would put the profile's words in the core.
pub profile: Option<String>,
/// Labels to stamp, by key.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
/// The payload file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// How the written envelope is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `deliver`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeliverOptions {
/// The spool's address: the directory its receiver bound.
pub address: String,
/// The message, as JSON text. The verb takes its message from exactly one of
/// this, `file`, and stdin, and refuses more than one by name.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
/// The file holding the message.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// Seconds to wait for the message to be taken before it is withdrawn.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wait: Option<u64>,
}
/// The options of `inbox carried`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InboxCarriedOptions {
/// The carry store to list.
pub store: String,
/// How the list is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `send`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SendOptions {
/// The queue to append to.
pub queue: QueueName,
/// The record file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `next`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NextOptions {
/// The queue to claim from.
pub queue: QueueName,
/// Who claims; the default consumer when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consumer: Option<ConsumerName>,
/// The asker this claim listens for, taking back what it abandoned.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asker: Option<Asker>,
/// How the claimed record is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `reply`.
// llmlint: ignore[invalid_states_unrepresentable] This is the language-neutral SDK wire object for a CLI invocation: preserving `position` and `correlation` as independently omitted fields keeps the generated clients' object shape stable, while clap and every SDK executor refuse both together before calling the bus. The library operation itself takes one `Option<&Correlation>` beside its distinct position-based queue operation, so this transport description is not an internal state retained after validation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReplyOptions {
/// The queue whose pending ask is answered.
pub queue: QueueName,
/// Where the pending record was claimed, as `next` printed it; absent, the
/// ask `correlation` names, or the one pending.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position: Option<Position>,
/// The correlation of the ask the reply answers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<Correlation>,
/// The reply file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `subscribe`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SubscribeOptions {
/// The queue to stream.
pub queue: QueueName,
/// The predicate ending the stream: inline JSON, or a path to a YAML
/// document.
// llmlint: ignore[invalid_states_unrepresentable] these options are the argv an SDK client hands `subscribe`, which takes the predicate as a spec — inline JSON or a path to a YAML file, as `--filter` does — and refuses one that is not a predicate by name at that boundary; a typed `Predicate` here could not carry the path form.
pub until: String,
/// Seconds to wait before giving up; no bound when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
/// How each record is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `status`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StatusOptions {
/// The queue to report; every declared queue when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub queue: Option<QueueName>,
/// How the report is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `transports`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TransportsOptions {
/// How the list is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `schemas`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemasOptions {
/// How the cache is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `schemas clear`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemasClearOptions {
/// How the count is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `schemas fetch`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemasFetchOptions {
/// The links to resolve; every link the configuration names when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub links: Option<Vec<SchemaLink>>,
/// The configuration whose links are resolved when none is named.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// How the report is rendered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
/// The options of `validate`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValidateOptions {
/// The queue whose validators judge the record.
pub queue: QueueName,
/// The record file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `ask`.
// llmlint: ignore[invalid_states_unrepresentable] This is the language-neutral SDK wire object, where each named field must remain a top-level capability binding. Clap and every generated SDK executor validate that `correlation` is not combined with `blocking`, `about`, or `file` before an invocation reaches `Bus::ask`; encoding the modes as a nested Rust enum would change those shared option keys and cease to describe the CLI boundary this schema exists to generate.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[schemars(rename = "AskOptions")]
pub struct AskVerbOptions {
/// The queue to ask on.
pub queue: QueueName,
/// Whether the asker waits on the answer.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking: Option<bool>,
/// Who asks.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asker: Option<Asker>,
/// What the question is about.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub about: Option<Address>,
/// Seconds to wait for the answer; no bound when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
/// Listen again for the question this correlation minted, raising nothing.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<Correlation>,
/// The question file; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// The options of `serve`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[schemars(rename = "ServeOptions")]
pub struct ServeVerbOptions {
/// The queue the codec raises and asks on.
pub queue: QueueName,
/// The codec the frames are read with.
pub codec: CodecName,
/// Seconds the session serves before it stops of its own accord.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_seconds: Option<NonZeroU64>,
/// Who the session listens for.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asker: Option<Asker>,
/// A file of frames, one per line; stdin when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
/// The configuration file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<String>,
/// The transport directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_dir: Option<String>,
/// The registry directory whose schemas are registered beside the layout's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
/// What `ask` answered. Every answer names itself in `answer`, and only a
/// reply carries a `reply`: a caller reading the reply member of a timeout, an
/// abandoned listener or a refusal reads nothing.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "answer", rename_all = "lowercase")]
pub enum Asked {
/// A reply record echoing the question's correlation.
Reply {
/// The question's correlation.
correlation: Correlation,
/// The reply record, as the answer queue holds it.
reply: Value,
},
/// The wait elapsed; the question stands.
Timeout {
/// The question's correlation.
correlation: Correlation,
},
/// The question was abandoned, and nobody re-attended it.
Abandoned {
/// The question's correlation.
correlation: Correlation,
},
/// The bus refused the question, or the reply to it.
Refused {
/// The question's correlation, when the question was asked.
#[serde(default, skip_serializing_if = "Option::is_none")]
correlation: Option<Correlation>,
/// Why.
reason: String,
},
}
/// What `validate` judged: the queue, and the verdict its validators reached.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Validated {
/// The queue the record was judged for.
pub queue: QueueName,
/// The verdict: `pass`, `refuse` or `unjudged`, with the reason beside a
/// verdict that is not a pass.
#[serde(flatten)]
pub verdict: Verdict,
}
/// One record `send` or `reply` appended.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Sent {
/// The queue it landed on.
pub queue: QueueName,
/// The position after it.
pub position: Position,
/// The id it was given, on a queue that gives one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
}
/// The record `next` claimed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ClaimedRecord {
/// The queue it was claimed from.
pub queue: QueueName,
/// Where the claim was recorded: what `reply` names the record by.
pub position: Position,
/// The record's id, on a queue that gives one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
/// The record.
pub record: Value,
}
/// What `reply` did: the pending record it answered, and the records it
/// appended — a reply routed by its halves appends more than one, and one with
/// no verdict answers nothing.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Replied {
/// The pending record answered, or `null` when the reply carried nothing
/// for its answer queue.
pub answered: Option<ClaimedRecord>,
/// The correlation of the ask the reply bound to, when it carries one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<Correlation>,
/// Every record appended, in order.
pub sent: Vec<Sent>,
}
/// One line of a queue's log, as `subscribe` streams it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LogRecord {
/// The position after it.
pub position: Position,
/// The line: on an event queue, `{"event": ..., ...record}`.
pub record: Value,
}
/// One entry of `schema list`.
///
/// `family` and `version` restate `id` for a reader that does not parse ids, so
/// an entry whose restatement contradicts its id is refused on read rather than
/// handed back with two answers to the same question.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "SchemaEntryWire")]
pub struct SchemaEntry {
/// The full id.
pub id: SchemaId,
/// The id without its version.
pub family: String,
/// The version.
pub version: u32,
}
/// A `schema list` entry as it arrives, before its restatement is checked.
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(rename = "SchemaEntry")]
struct SchemaEntryWire {
/// The full id.
id: SchemaId,
/// The id without its version.
family: String,
/// The version.
version: u32,
}
impl TryFrom<SchemaEntryWire> for SchemaEntry {
type Error = String;
fn try_from(wire: SchemaEntryWire) -> Result<Self, Self::Error> {
let entry = Self::from(&wire.id);
if wire.family != entry.family || wire.version != entry.version {
return Err(format!(
"the schema entry for {} restates it as family {} at version {}, which its id contradicts",
wire.id, wire.family, wire.version
));
}
Ok(entry)
}
}
impl From<&SchemaId> for SchemaEntry {
fn from(id: &SchemaId) -> Self {
Self {
id: id.clone(),
family: id.family(),
version: id.version(),
}
}
}
/// One file of a `--registry` directory: a document and the id it is under.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RegistryDocument {
/// The id the document is registered under.
pub id: SchemaId,
/// The JSON Schema document.
pub schema: Value,
}
/// The output of `schemas`: the cache directory and every entry it holds.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SchemaCache {
/// The cache directory.
pub cache: String,
/// One entry per cached location and declared version, by location then
/// version.
pub entries: Vec<CachedBundle>,
}
/// The output of `schemas clear`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SchemasCleared {
/// The cache directory.
pub cache: String,
/// How many entries were removed.
pub removed: u64,
}
/// How `schemas fetch` ended for one link, named in `outcome`: a version for
/// every link that resolved, and a reason for one reused or failed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "outcome", rename_all = "lowercase", deny_unknown_fields)]
pub enum FetchedLink {
/// A `file://` or bare-path bundle, read.
Read {
/// The link, as named.
link: SchemaLink,
/// The version the bundle declares.
version: BundleVersion,
},
/// Fetched from the origin, and stored when the link is pinned.
Fetched {
/// The link, as named.
link: SchemaLink,
/// The version the bundle declares.
version: BundleVersion,
},
/// The cached entry, confirmed current by the origin.
Confirmed {
/// The link, as named.
link: SchemaLink,
/// The version the cached bundle declares.
version: BundleVersion,
},
/// The cached entry, reused because its revalidation could not be made.
Reused {
/// The link, as named.
link: SchemaLink,
/// The version the cached bundle declares.
version: BundleVersion,
/// Why the revalidation failed.
reason: String,
},
/// Not resolved.
Failed {
/// The link, as named.
link: SchemaLink,
/// Why not.
reason: String,
},
}
/// The output of `schemas fetch`: one report per link, in the order named.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SchemasFetched {
/// Each link and how it ended.
pub links: Vec<FetchedLink>,
}
/// What one vocabulary declares, as the SDK manifest carries it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
pub struct VocabularyManifest {
/// The profile name.
pub name: &'static str,
/// The reserved label keys.
pub reserved: &'static [Reserved],
/// The top-level dimensions.
pub dimensions: &'static [Reserved],
/// The source word an emitter stamps when its caller names none.
pub default_source: &'static str,
}
/// Every shared contract root, and the capability manifest beside them.
#[derive(Debug, Serialize)]
pub struct Bundle {
/// The declared capability surface: what each SDK must expose, and which
/// CLI flag each of its options renders to.
pub capabilities: &'static [Capability],
/// The vocabulary the roots below are generated over.
pub vocabulary: VocabularyManifest,
/// The envelope over that vocabulary.
pub envelope: Schema,
/// The filter over that vocabulary.
pub filter: Schema,
/// One matcher of that filter.
pub matcher: Schema,
/// `<namespace>.<name>@<version>`.
pub schema_id: Schema,
/// One file of a `--registry` directory.
pub registry_document: Schema,
/// The output of `schema list`.
pub schema_list: Schema,
/// The output of `deliver`: the receiver's disposition, in the vocabulary of
/// the message family the spool carries.
pub disposition: Schema,
/// One line of `inbox carried`.
pub carried_entry: Schema,
/// The configuration file every queue verb reads, `onemessagebus.yaml`.
pub config: Schema,
/// The document a configuration's `schemas` links serve: a version, the
/// registry documents it publishes, and the layouts it declares as data.
pub schema_bundle: Schema,
/// One layout declared as data, as a bundle's `layouts` carries it: what a
/// configuration's `profile` names when no compiled-in layout has the name.
pub layout: Schema,
/// One line of `send`: a record appended.
pub sent: Schema,
/// The output of `next`: the record claimed.
pub claimed: Schema,
/// The output of `reply`.
pub replied: Schema,
/// One line of `subscribe`: a line of a queue's log.
pub log_record: Schema,
/// The output of `status`: one report per queue.
pub queue_statuses: Schema,
/// The output of `transports`: every kind this build can open.
pub transport_kinds: Schema,
/// The output of `validate`: the verdict a queue's validators reached.
pub validated: Schema,
/// The output of `ask`: the answer, named.
pub asked: Schema,
/// The output of `schemas`: the schema cache and its entries.
pub schema_cache: Schema,
/// The output of `schemas clear`: how many entries were removed.
pub schemas_cleared: Schema,
/// The output of `schemas fetch`: how each link resolved.
pub schemas_fetched: Schema,
/// One line of `serve`: the response to one frame, in the protocol of the
/// codec served.
pub codec_response: Schema,
/// The option contracts, by the root each capability names.
pub options: BTreeMap<&'static str, Schema>,
/// Every message the registry holds, by id.
pub messages: BTreeMap<String, Value>,
}
/// The bundle for vocabulary `V`, carrying every message `registry` holds.
#[must_use]
pub fn bundle<V: Vocabulary>(registry: &Registry) -> Bundle {
let mut options = BTreeMap::new();
options.insert("schema_list_options", schema_for!(SchemaListOptions));
options.insert("schema_check_options", schema_for!(SchemaCheckOptions));
options.insert("schema_gen_options", schema_for!(SchemaGenOptions));
options.insert(
"schema_register_options",
schema_for!(SchemaRegisterOptions),
);
options.insert("events_merge_options", schema_for!(EventsMergeOptions));
options.insert("events_emit_options", schema_for!(EventsEmitOptions));
options.insert("deliver_options", schema_for!(DeliverOptions));
options.insert("inbox_carried_options", schema_for!(InboxCarriedOptions));
options.insert("send_options", schema_for!(SendOptions));
options.insert("next_options", schema_for!(NextOptions));
options.insert("reply_options", schema_for!(ReplyOptions));
options.insert("subscribe_options", schema_for!(SubscribeOptions));
options.insert("status_options", schema_for!(StatusOptions));
options.insert("transports_options", schema_for!(TransportsOptions));
options.insert("validate_options", schema_for!(ValidateOptions));
options.insert("schemas_options", schema_for!(SchemasOptions));
options.insert("schemas_clear_options", schema_for!(SchemasClearOptions));
options.insert("schemas_fetch_options", schema_for!(SchemasFetchOptions));
options.insert("ask_options", schema_for!(AskVerbOptions));
options.insert("serve_options", schema_for!(ServeVerbOptions));
Bundle {
capabilities: CAPABILITIES,
vocabulary: VocabularyManifest {
name: V::NAME,
reserved: V::RESERVED,
dimensions: V::DIMENSIONS,
default_source: V::DEFAULT_SOURCE,
},
envelope: schema_for!(Envelope<V>),
filter: schema_for!(Filter<V>),
matcher: schema_for!(Matcher<V>),
schema_id: schema_for!(SchemaId),
registry_document: schema_for!(RegistryDocument),
schema_list: schema_for!(Vec<SchemaEntry>),
disposition: schemars::json_schema!({
"title": "Disposition",
"description": "What the receiver answered the message with: any JSON value, in the vocabulary of the message family the spool carries."
}),
carried_entry: schema_for!(CarriedEntry),
config: schema_for!(Config),
schema_bundle: schema_for!(SchemaBundle),
layout: schema_for!(LayoutDocument),
sent: schema_for!(Sent),
claimed: schema_for!(ClaimedRecord),
replied: schema_for!(Replied),
log_record: schema_for!(LogRecord),
queue_statuses: schema_for!(Vec<QueueStatus>),
transport_kinds: schema_for!(Vec<KindEntry>),
validated: schema_for!(Validated),
asked: schema_for!(Asked),
schema_cache: schema_for!(SchemaCache),
schemas_cleared: schema_for!(SchemasCleared),
schemas_fetched: schema_for!(SchemasFetched),
codec_response: schemars::json_schema!({
"title": "CodecResponse",
"description": "The response to one frame, in the protocol of the codec served: one JSON object per line.",
"type": "object"
}),
options,
messages: registry
.ids()
.into_iter()
.filter_map(|id| {
registry
.schema(&id)
.map(|document| (id.to_string(), document.clone()))
})
.collect(),
}
}
/// Why a document could not be rendered for a language.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum GenerateError {
/// This build renders no declaration for the language.
#[error("{id}: `{lang}` declarations are generated by the {lang} SDK package, not by this build; ask for `json` or `rust`")]
Unsupported {
/// The id asked for.
id: SchemaId,
/// The language.
lang: &'static str,
},
/// The document uses a construct the Rust renderer does not cover.
#[error("{id}: cannot render {at} as Rust: {why}")]
Unrenderable {
/// The id asked for.
id: SchemaId,
/// Where in the document.
at: String,
/// What is there.
why: String,
},
}
/// The document registered under `id`, rendered for `lang`.
///
/// `json` is the document itself, pretty-printed; `rust` is a declaration
/// that compiles with serde and schemars and regenerates the document.
///
/// # Errors
///
/// [`GenerateError`] for a language this build does not render, or a
/// document the Rust renderer cannot express.
pub fn generate(lang: Lang, id: &SchemaId, document: &Value) -> Result<String, GenerateError> {
match lang {
Lang::Json => Ok(format!(
"{}\n",
serde_json::to_string_pretty(document).unwrap_or_default()
)),
Lang::Rust => rust::render(id, document),
Lang::Python | Lang::Typescript => Err(GenerateError::Unsupported {
id: id.clone(),
lang: lang.as_str(),
}),
}
}
/// The Rust rendering of a JSON Schema document, in the subset schemars
/// generates for plain data: objects with typed properties, arrays, string
/// enums, nullable fields, and `$defs` references.
mod rust {
use super::{GenerateError, Map, SchemaId, Value};
use std::collections::BTreeMap;
use std::fmt::Write as _;
/// Every declaration the document needs, root first, then its `$defs` in
/// order.
pub(super) fn render(id: &SchemaId, document: &Value) -> Result<String, GenerateError> {
let mut out = String::new();
let _ = writeln!(
out,
"// Generated by `onemessagebus schema gen --lang rust {id}`; regenerates the same document."
);
let _ = writeln!(out, "// Do not edit by hand.");
let title = document
.get("title")
.and_then(Value::as_str)
.ok_or_else(|| unrenderable(id, "", "the document has no title to name the type by"))?;
// Every type name is checked before any declaration is rendered, so a
// `$ref` naming a declared type always names one that renders.
let mut types = Names::new("type");
let ident = types.admit(id, "/title", "title", title, title.to_owned())?;
let mut declarations = vec![(title, ident, String::new(), document)];
if let Some(defs) = document.get("$defs").and_then(Value::as_object) {
for (name, def) in defs {
let at = format!("/$defs/{}", pointer_token(name));
let ident = types.admit(id, &at, "$defs name", name, name.clone())?;
declarations.push((name, ident, at, def));
}
}
for (name, ident, at, schema) in declarations {
out.push('\n');
declaration(id, name, &ident, &at, schema, &mut out)?;
}
Ok(out)
}
/// Keywords Rust accepts as raw identifiers: strict, reserved, and those of
/// later editions, so a declaration compiles under any of them.
const RAW_KEYWORDS: &[&str] = &[
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "do",
"dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl", "in",
"let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
"return", "static", "struct", "trait", "true", "try", "type", "typeof", "unsafe",
"unsized", "use", "virtual", "where", "while", "yield",
];
/// Keywords Rust refuses even as raw identifiers.
const UNRAW_KEYWORDS: &[&str] = &["crate", "self", "Self", "super"];
/// The identifiers one namespace of the generated source has handed out —
/// the types of the module, the fields of a struct, the variants of an
/// enum — so two names from the document cannot become the same one.
struct Names<'a> {
/// What an identifier here declares, for a refusal.
declares: &'static str,
/// Each identifier, by what named it and the name it was derived from;
/// `None` for one the renderer declares itself.
taken: BTreeMap<String, Option<(&'a str, &'a str)>>,
}
impl<'a> Names<'a> {
fn new(declares: &'static str) -> Self {
Self {
declares,
taken: BTreeMap::new(),
}
}
/// Hold `ident` for the renderer's own use.
fn reserve(&mut self, ident: &str) {
self.taken.insert(ident.to_owned(), None);
}
/// `derived`, the identifier the renderer spells `name` as, once it is
/// known to be one Rust accepts and no other name here became it.
fn admit(
&mut self,
id: &SchemaId,
at: &str,
what: &'a str,
name: &'a str,
derived: String,
) -> Result<String, GenerateError> {
let ident = identifier(id, at, what, name, derived)?;
match self.taken.get(&ident) {
Some(Some((named, earlier))) => Err(unrenderable(
id,
at,
&format!(
"the {named} {earlier:?} and the {what} {name:?} both become the {} `{ident}`",
self.declares
),
)),
Some(None) => Err(unrenderable(
id,
at,
&format!(
"the {what} {name:?} becomes `{ident}`, the {} the renderer declares for every other key",
self.declares
),
)),
None => {
self.taken.insert(ident.clone(), Some((what, name)));
Ok(ident)
}
}
}
}
/// `derived`, the identifier the renderer spells `name` as, checked to be
/// one Rust accepts: a keyword is written raw where Rust allows that, since
/// a name from the document is spliced into source verbatim.
fn identifier(
id: &SchemaId,
at: &str,
what: &str,
name: &str,
derived: String,
) -> Result<String, GenerateError> {
let Some(defect) = defect_of(name, &derived) else {
return Ok(if RAW_KEYWORDS.contains(&derived.as_str()) {
format!("r#{derived}")
} else {
derived
});
};
let spelled = if derived == name || derived.is_empty() {
String::new()
} else {
format!(" (as `{derived}`)")
};
Err(unrenderable(
id,
at,
&format!("the {what} {name:?}{spelled} is not a Rust identifier: {defect}"),
))
}
/// What keeps `ident`, derived from `name`, from being a Rust identifier.
fn defect_of(name: &str, ident: &str) -> Option<String> {
let Some(first) = ident.chars().next() else {
return Some(if name.is_empty() {
"it is empty".to_owned()
} else {
"nothing of it is left once mapped".to_owned()
});
};
if first.is_ascii_digit() {
return Some("it starts with a digit".to_owned());
}
if let Some(ch) = ident
.chars()
.find(|ch| !ch.is_ascii_alphanumeric() && *ch != '_')
{
return Some(format!(
"{ch:?} is not an ASCII letter, digit or underscore"
));
}
if ident == "_" {
return Some("a lone `_` is a pattern, not a name".to_owned());
}
if UNRAW_KEYWORDS.contains(&ident) {
return Some(format!(
"`{ident}` is a keyword Rust does not take even as a raw identifier"
));
}
None
}
/// `name` escaped as one reference token of a JSON pointer.
fn pointer_token(name: &str) -> String {
name.replace('~', "~0").replace('/', "~1")
}
fn unrenderable(id: &SchemaId, at: &str, why: &str) -> GenerateError {
GenerateError::Unrenderable {
id: id.clone(),
at: if at.is_empty() {
"the document".to_owned()
} else {
at.to_owned()
},
why: why.to_owned(),
}
}
fn doc(description: Option<&Value>, indent: &str, out: &mut String) {
if let Some(text) = description.and_then(Value::as_str) {
for line in text.lines() {
let _ = writeln!(
out,
"{indent}///{}{line}",
if line.is_empty() { "" } else { " " }
);
}
}
}
/// The declaration of `schema`, which the document names `name` and which
/// sits at the JSON pointer `at`, as the type `ident`.
fn declaration(
id: &SchemaId,
name: &str,
ident: &str,
at: &str,
schema: &Value,
out: &mut String,
) -> Result<(), GenerateError> {
doc(schema.get("description"), "", out);
if let Some(variants) = words_of(schema) {
let _ = writeln!(
out,
"#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]"
);
let _ = writeln!(out, "pub enum {ident} {{");
let mut taken = Names::new("variant");
for (word, description, within) in variants {
let word = word
.as_str()
.ok_or_else(|| unrenderable(id, name, "an enum value that is not a string"))?;
let variant = taken.admit(
id,
&format!("{at}/{within}"),
"enum value",
word,
pascal(word),
)?;
doc(description, " ", out);
let _ = writeln!(out, " #[serde(rename = \"{}\")]", escape(word));
let _ = writeln!(out, " {variant},");
}
let _ = writeln!(out, "}}");
return Ok(());
}
if let Some(inner) = scalar_newtype(id, name, schema)? {
let _ = writeln!(
out,
"#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]"
);
let _ = writeln!(out, "pub struct {ident}(pub {inner});");
return Ok(());
}
let properties = schema
.get("properties")
.and_then(Value::as_object)
.ok_or_else(|| {
unrenderable(
id,
name,
"a schema that is neither an object with properties, a string enum, nor a named scalar",
)
})?;
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.map(|required| required.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let closed = schema.get("additionalProperties") == Some(&Value::Bool(false));
let flattened = !closed
&& schema
.get("additionalProperties")
.is_some_and(|extra| extra != &Value::Bool(false));
let _ = writeln!(
out,
"#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]"
);
if closed {
let _ = writeln!(out, "#[serde(deny_unknown_fields)]");
}
let _ = writeln!(out, "pub struct {ident} {{");
let mut taken = Names::new("field");
if flattened {
taken.reserve("extra");
}
for (property, property_schema) in properties {
let field = taken.admit(
id,
&format!("{at}/properties/{}", pointer_token(property)),
"property name",
property,
field_name(property),
)?;
doc(property_schema.get("description"), " ", out);
let at = format!("{name}.{property}");
let (nullable, inner) = nullable(property_schema);
let ty = type_of(id, &at, &inner)?;
let is_required = required.contains(&property.as_str());
let mut attributes = Vec::new();
if field != *property {
attributes.push(format!("rename = \"{}\"", escape(property)));
}
let ty = if nullable {
if !is_required {
attributes.push("default".to_owned());
attributes.push("skip_serializing_if = \"Option::is_none\"".to_owned());
}
format!("Option<{ty}>")
} else {
if !is_required {
attributes.push("default".to_owned());
}
ty
};
if !attributes.is_empty() {
let _ = writeln!(out, " #[serde({})]", attributes.join(", "));
}
let _ = writeln!(out, " pub {field}: {ty},");
}
if flattened {
let _ = writeln!(out, " /// Every other key, carried through untouched.");
let _ = writeln!(out, " #[serde(flatten)]");
let _ = writeln!(
out,
" pub extra: serde_json::Map<String, serde_json::Value>,"
);
}
let _ = writeln!(out, "}}");
Ok(())
}
/// The type a named scalar is a newtype over: a schema that is one scalar
/// `type` beside nothing a newtype would not regenerate — its description,
/// an integer's format, and the zero minimum schemars writes beside an
/// unsigned one. `None` for any other schema, which is left to be refused
/// by name: a pattern or a length on a scalar is a keyword no newtype
/// declaration regenerates.
fn scalar_newtype(
id: &SchemaId,
at: &str,
schema: &Value,
) -> Result<Option<String>, GenerateError> {
let Some(object) = schema.as_object() else {
return Ok(None);
};
let scalar = matches!(
object.get("type").and_then(Value::as_str),
Some("string" | "boolean" | "integer" | "number")
);
let unsigned = object
.get("format")
.and_then(Value::as_str)
.is_some_and(|format| format.starts_with("uint"));
let regenerates = object.iter().all(|(key, value)| match key.as_str() {
"type" | "description" | "format" => true,
"minimum" => unsigned && value.as_u64() == Some(0),
_ => false,
});
if !scalar || !regenerates {
return Ok(None);
}
let inner: Map<String, Value> = object
.iter()
.filter(|(key, _)| key.as_str() != "description")
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
type_of(id, at, &Value::Object(inner)).map(Some)
}
/// The words of a closed string set, each with its description and its
/// JSON pointer below the set: schemars spells an undocumented set as
/// `enum` and a documented one as `oneOf` string constants.
fn words_of(schema: &Value) -> Option<Vec<(&Value, Option<&Value>, String)>> {
if let Some(words) = schema.get("enum").and_then(Value::as_array) {
return Some(
words
.iter()
.enumerate()
.map(|(index, word)| (word, None, format!("enum/{index}")))
.collect(),
);
}
let branches = schema.get("oneOf")?.as_array()?;
let mut words = Vec::new();
for (index, branch) in branches.iter().enumerate() {
if branch.get("type") != Some(&Value::String("string".to_owned())) {
return None;
}
words.push((
branch.get("const")?,
branch.get("description"),
format!("oneOf/{index}/const"),
));
}
Some(words)
}
/// Whether a property schema admits null, and the schema of the value when
/// it does not.
fn nullable(schema: &Value) -> (bool, Value) {
if let Some(types) = schema.get("type").and_then(Value::as_array) {
let words: Vec<&str> = types.iter().filter_map(Value::as_str).collect();
if words.contains(&"null") && words.len() == 2 {
let mut inner = schema.clone();
let other = words.iter().find(|word| **word != "null").copied();
if let (Some(other), Some(object)) = (other, inner.as_object_mut()) {
object.insert("type".to_owned(), Value::String(other.to_owned()));
}
return (true, inner);
}
}
if let Some(branches) = schema.get("anyOf").and_then(Value::as_array) {
let null = Value::Object(Map::from_iter([(
"type".to_owned(),
Value::String("null".to_owned()),
)]));
if branches.len() == 2 && branches.contains(&null) {
if let Some(other) = branches.iter().find(|branch| **branch != null) {
return (true, other.clone());
}
}
}
(false, schema.clone())
}
fn type_of(id: &SchemaId, at: &str, schema: &Value) -> Result<String, GenerateError> {
// `true`, or an object saying nothing but a description: anything at all.
let says_nothing = schema
.as_object()
.is_some_and(|object| object.keys().all(|key| key == "description"));
if schema == &Value::Bool(true) || says_nothing {
return Ok("serde_json::Value".to_owned());
}
if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
let target = reference
.strip_prefix("#/$defs/")
.ok_or_else(|| unrenderable(id, at, "a reference outside the document's $defs"))?;
return identifier(id, at, "reference target", target, target.to_owned());
}
let kind = schema
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| unrenderable(id, at, "a schema with no single type"))?;
let format = schema.get("format").and_then(Value::as_str);
Ok(match (kind, format) {
("string", _) => "String".to_owned(),
("boolean", _) => "bool".to_owned(),
("integer", Some("uint64")) => "u64".to_owned(),
("integer", Some("uint32")) => "u32".to_owned(),
("integer", Some("uint16")) => "u16".to_owned(),
("integer", Some("uint8")) => "u8".to_owned(),
("integer", Some("uint")) => "usize".to_owned(),
("integer", Some("int64")) => "i64".to_owned(),
("integer", Some("int32")) => "i32".to_owned(),
("integer", Some("int16")) => "i16".to_owned(),
("integer", Some("int8")) => "i8".to_owned(),
("integer", Some("int")) => "isize".to_owned(),
("integer", _) => "i64".to_owned(),
("number", Some("float")) => "f32".to_owned(),
("number", _) => "f64".to_owned(),
("array", _) => {
let items = schema
.get("items")
.ok_or_else(|| unrenderable(id, at, "an array with no item schema"))?;
format!("Vec<{}>", type_of(id, at, items)?)
}
("object", _) => {
if schema.get("properties").is_some() {
return Err(unrenderable(
id,
at,
"an inline object with properties; declare it in $defs",
));
}
match schema.get("additionalProperties") {
None | Some(Value::Bool(true)) => {
"serde_json::Map<String, serde_json::Value>".to_owned()
}
Some(values) => format!(
"std::collections::BTreeMap<String, {}>",
type_of(id, at, values)?
),
}
}
(other, _) => return Err(unrenderable(id, at, &format!("a `{other}` schema"))),
})
}
/// The snake-case spelling of `property`, before it is checked.
fn field_name(property: &str) -> String {
let mut out = String::new();
for (index, ch) in property.chars().enumerate() {
if ch.is_ascii_uppercase() {
if index > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else if ch == '-' {
out.push('_');
} else {
out.push(ch);
}
}
out
}
/// The Pascal-case spelling of `word`, before it is checked.
fn pascal(word: &str) -> String {
let mut out = String::new();
let mut upper = true;
for ch in word.chars() {
if ch == '-' || ch == '_' || ch == ' ' {
upper = true;
} else if upper {
out.push(ch.to_ascii_uppercase());
upper = false;
} else {
out.push(ch);
}
}
out
}
fn escape(text: &str) -> String {
text.replace('\\', "\\\\").replace('"', "\\\"")
}
}
impl Bundle {
/// The bundle as one JSON document.
#[must_use]
pub fn to_json(&self) -> String {
let mut out = serde_json::to_string_pretty(self).unwrap_or_default();
let _ = writeln!(out);
out
}
}