use core::fmt;
use std::sync::Arc;
use crate::domain::Feature;
use crate::domain::ir::FeatureSet;
use crate::domain::ir::{Annotations, Extensions, JsonObject, ResourceIcon};
use crate::domain::registry::auth::{Scope, ScopeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PrimitiveKind {
Tool,
Resource,
ResourceTemplate,
Prompt,
}
impl PrimitiveKind {
pub const ALL: [Self; 4] = [
Self::Tool,
Self::Resource,
Self::ResourceTemplate,
Self::Prompt,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Tool => "tool",
Self::Resource => "resource",
Self::ResourceTemplate => "resource_template",
Self::Prompt => "prompt",
}
}
#[must_use]
pub const fn feature(self) -> Feature {
match self {
Self::Tool => Feature::Tools,
Self::Resource | Self::ResourceTemplate => Feature::Resources,
Self::Prompt => Feature::Prompts,
}
}
}
impl fmt::Display for PrimitiveKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrimitiveId(Arc<str>);
impl PrimitiveId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_arc(self) -> Arc<str> {
self.0
}
}
impl From<&str> for PrimitiveId {
fn from(text: &str) -> Self {
Self(Arc::from(text))
}
}
impl From<String> for PrimitiveId {
fn from(text: String) -> Self {
Self(Arc::from(text))
}
}
impl From<Arc<str>> for PrimitiveId {
fn from(text: Arc<str>) -> Self {
Self(text)
}
}
impl fmt::Display for PrimitiveId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrimitiveMeta {
kind: PrimitiveKind,
requires: FeatureSet,
scopes: ScopeSet,
}
impl PrimitiveMeta {
#[must_use]
pub const fn new(kind: PrimitiveKind) -> Self {
Self {
kind,
requires: FeatureSet::empty(),
scopes: ScopeSet::empty(),
}
}
#[must_use]
pub const fn requires(mut self, features: FeatureSet) -> Self {
self.requires = features;
self
}
#[must_use]
pub const fn scopes(mut self, scopes: ScopeSet) -> Self {
self.scopes = scopes;
self
}
#[must_use]
pub const fn kind(self) -> PrimitiveKind {
self.kind
}
#[must_use]
pub const fn required_features(self) -> FeatureSet {
self.requires
}
#[must_use]
pub const fn required_scopes(self) -> ScopeSet {
self.scopes
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tool {
pub name: PrimitiveId,
pub title: Option<String>,
pub description: Option<String>,
pub input_schema: JsonObject,
pub output_schema: Option<JsonObject>,
pub annotations: Option<ToolAnnotations>,
pub icons: Option<Vec<ResourceIcon>>,
pub extensions: Extensions,
}
impl Tool {
pub fn new(
name: impl Into<PrimitiveId>,
input_schema: &str,
) -> Result<Self, crate::domain::ir::JsonObjectError> {
Ok(Self {
name: name.into(),
title: None,
description: None,
input_schema: JsonObject::new(input_schema)?,
output_schema: None,
annotations: None,
icons: None,
extensions: Extensions::empty(),
})
}
#[must_use]
pub fn described(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_output_schema(
mut self,
schema: &str,
) -> Result<Self, crate::domain::ir::JsonObjectError> {
self.output_schema = Some(JsonObject::new(schema)?);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ToolAnnotations {
pub title: Option<String>,
pub read_only_hint: Option<bool>,
pub destructive_hint: Option<bool>,
pub idempotent_hint: Option<bool>,
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Resource {
pub uri: PrimitiveId,
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub size: Option<u64>,
pub annotations: Option<Annotations>,
pub icons: Option<Vec<ResourceIcon>>,
pub extensions: Extensions,
}
impl Resource {
#[must_use]
pub fn new(uri: impl Into<PrimitiveId>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
size: None,
annotations: None,
icons: None,
extensions: Extensions::empty(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResourceTemplate {
pub uri_template: PrimitiveId,
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub annotations: Option<Annotations>,
pub icons: Option<Vec<ResourceIcon>>,
pub extensions: Extensions,
}
impl ResourceTemplate {
#[must_use]
pub fn new(uri_template: impl Into<PrimitiveId>, name: impl Into<String>) -> Self {
Self {
uri_template: uri_template.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
annotations: None,
icons: None,
extensions: Extensions::empty(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PromptArgument {
pub name: String,
pub description: Option<String>,
pub title: Option<String>,
pub required: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Prompt {
pub name: PrimitiveId,
pub title: Option<String>,
pub description: Option<String>,
pub arguments: Vec<PromptArgument>,
pub icons: Option<Vec<ResourceIcon>>,
pub extensions: Extensions,
}
impl Prompt {
#[must_use]
pub fn new(name: impl Into<PrimitiveId>) -> Self {
Self {
name: name.into(),
title: None,
description: None,
arguments: Vec::new(),
icons: None,
extensions: Extensions::empty(),
}
}
#[must_use]
pub fn with_argument(mut self, name: impl Into<String>, required: bool) -> Self {
self.arguments.push(PromptArgument {
name: name.into(),
description: None,
title: None,
required,
});
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Primitive {
Tool(Tool),
Resource(Resource),
ResourceTemplate(ResourceTemplate),
Prompt(Prompt),
}
impl Primitive {
#[must_use]
pub const fn kind(&self) -> PrimitiveKind {
match self {
Self::Tool(_) => PrimitiveKind::Tool,
Self::Resource(_) => PrimitiveKind::Resource,
Self::ResourceTemplate(_) => PrimitiveKind::ResourceTemplate,
Self::Prompt(_) => PrimitiveKind::Prompt,
}
}
#[must_use]
pub const fn id(&self) -> &PrimitiveId {
match self {
Self::Tool(tool) => &tool.name,
Self::Resource(resource) => &resource.uri,
Self::ResourceTemplate(template) => &template.uri_template,
Self::Prompt(prompt) => &prompt.name,
}
}
#[must_use]
pub fn meta(&self) -> PrimitiveMeta {
let mut meta = PrimitiveMeta::new(self.kind());
if let Self::Tool(tool) = self
&& tool.output_schema.is_some()
{
meta = meta.requires(FeatureSet::empty().with(Feature::ToolStructuredOutput));
}
meta
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Registered {
primitive: Primitive,
policy: PrimitiveMeta,
}
impl Registered {
#[must_use]
pub fn new(primitive: Primitive) -> Self {
let policy = primitive.meta();
Self { primitive, policy }
}
#[must_use]
pub const fn primitive(&self) -> &Primitive {
&self.primitive
}
#[must_use]
pub const fn id(&self) -> &PrimitiveId {
self.primitive.id()
}
#[must_use]
pub const fn kind(&self) -> PrimitiveKind {
self.primitive.kind()
}
#[must_use]
pub const fn meta(&self) -> &PrimitiveMeta {
&self.policy
}
#[must_use]
pub const fn requiring(mut self, features: FeatureSet) -> Self {
self.policy = self
.policy
.requires(self.policy.required_features().union(features));
self
}
#[must_use]
pub const fn requiring_scope(mut self, scope: Scope) -> Self {
self.policy = self
.policy
.scopes(self.policy.required_scopes().with(scope));
self
}
}
impl From<Primitive> for Registered {
fn from(primitive: Primitive) -> Self {
Self::new(primitive)
}
}
impl From<Tool> for Primitive {
fn from(tool: Tool) -> Self {
Self::Tool(tool)
}
}
impl From<Resource> for Primitive {
fn from(resource: Resource) -> Self {
Self::Resource(resource)
}
}
impl From<ResourceTemplate> for Primitive {
fn from(template: ResourceTemplate) -> Self {
Self::ResourceTemplate(template)
}
}
impl From<Prompt> for Primitive {
fn from(prompt: Prompt) -> Self {
Self::Prompt(prompt)
}
}
#[cfg(test)]
mod tests {
use core::cmp::Ordering;
use std::sync::Arc;
use super::{
Primitive, PrimitiveId, PrimitiveKind, PrimitiveMeta, Prompt, Resource, ResourceTemplate,
Tool,
};
use crate::domain::Feature;
use crate::domain::ir::FeatureSet;
use crate::domain::ir::JsonObjectError;
use crate::domain::registry::auth::{Scope, ScopeSet};
#[test]
fn the_key_is_the_field_the_list_method_uses() -> Result<(), JsonObjectError> {
let cases = [
(
Primitive::from(Tool::new("t", "{}")?),
PrimitiveKind::Tool,
"t",
),
(
Primitive::from(Resource::new("file:///a", "a")),
PrimitiveKind::Resource,
"file:///a",
),
(
Primitive::from(ResourceTemplate::new("file:///{id}", "by-id")),
PrimitiveKind::ResourceTemplate,
"file:///{id}",
),
(
Primitive::from(Prompt::new("greet")),
PrimitiveKind::Prompt,
"greet",
),
];
for (primitive, kind, key) in cases {
assert_eq!(primitive.kind(), kind);
assert_eq!(primitive.id().as_str(), key);
}
Ok(())
}
#[test]
fn ordering_agrees_in_both_directions() {
for a in PrimitiveKind::ALL {
for b in PrimitiveKind::ALL {
let partial = a.partial_cmp(&b);
assert_eq!(partial, Some(a.cmp(&b)), "{a} against {b}");
assert_eq!(a < b, partial == Some(Ordering::Less), "{a} against {b}");
assert_eq!(a > b, partial == Some(Ordering::Greater), "{a} against {b}");
}
}
}
#[test]
fn the_kind_order_is_the_specification_order() {
assert_eq!(
PrimitiveKind::ALL,
[
PrimitiveKind::Tool,
PrimitiveKind::Resource,
PrimitiveKind::ResourceTemplate,
PrimitiveKind::Prompt,
]
);
}
#[test]
fn metadata_follows_the_payload() -> Result<(), JsonObjectError> {
let plain = Tool::new("t", "{}")?;
assert!(Primitive::from(plain).meta().required_features().is_empty());
let structured = Tool::new("t", "{}")?.with_output_schema(r#"{"type":"object"}"#)?;
let required = Primitive::from(structured).meta().required_features();
assert!(required.contains(Feature::ToolStructuredOutput));
Ok(())
}
#[test]
fn scopes_are_carried_by_the_metadata_not_the_payload() {
let meta = PrimitiveMeta::new(PrimitiveKind::Tool)
.scopes(ScopeSet::empty().with(Scope::Write))
.requires(FeatureSet::empty().with(Feature::Tools));
assert!(meta.required_scopes().contains(Scope::Write));
assert!(meta.required_features().contains(Feature::Tools));
assert_eq!(meta.kind(), PrimitiveKind::Tool);
}
#[test]
fn metadata_without_a_rule_requires_nothing() {
let meta = PrimitiveMeta::new(PrimitiveKind::Resource);
assert!(meta.required_features().is_empty());
assert!(meta.required_scopes().holds(ScopeSet::empty()));
}
#[test]
fn identifiers_share_their_bytes() {
let id = PrimitiveId::from("tools/call");
let clone = id.clone();
assert_eq!(id, clone);
assert!(Arc::ptr_eq(&id.clone().into_arc(), &clone.into_arc()));
assert_eq!(id.to_string(), "tools/call");
}
#[test]
fn a_non_object_schema_is_refused_at_the_boundary() {
assert!(Tool::new("t", "[]").is_err());
assert!(Tool::new("t", "{oops").is_err());
assert!(
Tool::new("t", "{}")
.and_then(|t| t.with_output_schema("[]"))
.is_err()
);
}
}