feat: add function definition contract

This commit is contained in:
Xuanwo
2026-08-11 23:11:01 +08:00
parent 1798ece362
commit 9d589bea44
3 changed files with 1309 additions and 35 deletions
+45 -35
View File
@@ -1,11 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
//! Immutable first-class Function and generated-column value model (B1a).
//! Immutable first-class Function and generated-column value model (B1a/B1c).
//!
//! This module defines transport and metadata value types only. It does not
//! provide catalogs, job execution, query planning, or generated-column runtime.
mod definition;
pub use definition::{FunctionCapability, FunctionDefinition, PythonFunctionDefinition};
use std::collections::HashSet;
use std::io::Cursor;
use std::sync::Arc;
@@ -251,6 +255,44 @@ impl FunctionSignature {
pub fn output(&self) -> &FunctionOutput {
&self.output
}
fn to_wire(&self) -> Result<SignatureWire> {
let parameters = self
.parameters
.iter()
.map(|parameter| {
Ok(ParameterWire {
name: parameter.name.clone(),
data_type_ipc: encode_type_ipc_b64(&parameter.data_type)?,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(SignatureWire {
parameters,
output: OutputWire {
data_type_ipc: encode_type_ipc_b64(&self.output.data_type)?,
nullable: self.output.nullable,
},
})
}
fn from_wire(wire: SignatureWire) -> Result<Self> {
let parameters = wire
.parameters
.into_iter()
.map(|parameter| {
Ok(FunctionParameter::new(
parameter.name,
decode_type_ipc_b64(&parameter.data_type_ipc)?,
))
})
.collect::<Result<Vec<_>>>()?;
let output = FunctionOutput::new(
decode_type_ipc_b64(&wire.output.data_type_ipc)?,
wire.output.nullable,
);
Self::try_new(parameters, output)
}
}
/// Immutable first-class function value (format version 1).
@@ -308,27 +350,10 @@ struct OutputWire {
impl Function {
fn to_wire(&self) -> Result<FunctionWire> {
let parameters = self
.signature
.parameters
.iter()
.map(|parameter| {
Ok(ParameterWire {
name: parameter.name.clone(),
data_type_ipc: encode_type_ipc_b64(&parameter.data_type)?,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(FunctionWire {
format_version: FORMAT_VERSION_V1,
id: self.id.value.clone(),
signature: SignatureWire {
parameters,
output: OutputWire {
data_type_ipc: encode_type_ipc_b64(&self.signature.output.data_type)?,
nullable: self.signature.output.nullable,
},
},
signature: self.signature.to_wire()?,
})
}
@@ -340,22 +365,7 @@ impl Function {
)));
}
let id = FunctionId::try_new(wire.id)?;
let parameters = wire
.signature
.parameters
.into_iter()
.map(|parameter| {
Ok(FunctionParameter::new(
parameter.name,
decode_type_ipc_b64(&parameter.data_type_ipc)?,
))
})
.collect::<Result<Vec<_>>>()?;
let output = FunctionOutput::new(
decode_type_ipc_b64(&wire.signature.output.data_type_ipc)?,
wire.signature.output.nullable,
);
let signature = FunctionSignature::try_new(parameters, output)?;
let signature = FunctionSignature::from_wire(wire.signature)?;
Ok(Self { id, signature })
}
}
+427
View File
@@ -0,0 +1,427 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
//! Immutable FunctionDefinition registration input (B1c / FF-007).
//!
//! These types are authoring/transport values only. They do not mint identity,
//! store digests/artifacts, or execute Python.
use std::collections::HashSet;
use std::fmt;
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::{FunctionSignature, SignatureWire, invalid_input};
use crate::Result;
const FORMAT_VERSION_V1: u32 = 1;
/// Immutable Python implementation description for a [`FunctionDefinition`].
///
/// The source body is carried on the trusted registration wire but is omitted
/// from [`Debug`] output.
#[derive(Clone, PartialEq, Eq)]
pub struct PythonFunctionDefinition {
module: String,
callable: String,
source: String,
python: String,
packages: Vec<String>,
}
impl PythonFunctionDefinition {
/// Create a Python implementation description.
///
/// Rejects empty `module`, `callable`, `source`, `python`, or any empty
/// package requirement, and rejects duplicate package requirement strings.
pub fn try_new(
module: impl Into<String>,
callable: impl Into<String>,
source: impl Into<String>,
python: impl Into<String>,
packages: Vec<String>,
) -> Result<Self> {
let module = module.into();
let callable = callable.into();
let source = source.into();
let python = python.into();
validate_python_fields(&module, &callable, &source, &python, &packages)?;
Ok(Self {
module,
callable,
source,
python,
packages,
})
}
/// Python module name.
pub fn module(&self) -> &str {
&self.module
}
/// Callable name within the module.
pub fn callable(&self) -> &str {
&self.callable
}
/// Source body submitted at the trusted registration boundary.
pub fn source(&self) -> &str {
&self.source
}
/// Requested Python runtime version string.
pub fn python(&self) -> &str {
&self.python
}
/// Ordered package requirements.
pub fn packages(&self) -> &[String] {
&self.packages
}
}
impl fmt::Debug for PythonFunctionDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PythonFunctionDefinition")
.field("module", &self.module)
.field("callable", &self.callable)
.field("source", &"<redacted>")
.field("python", &self.python)
.field("packages", &self.packages)
.finish()
}
}
fn validate_python_fields(
module: &str,
callable: &str,
source: &str,
python: &str,
packages: &[String],
) -> Result<()> {
if module.is_empty() {
return Err(invalid_input(
"PythonFunctionDefinition module must be non-empty",
));
}
if callable.is_empty() {
return Err(invalid_input(
"PythonFunctionDefinition callable must be non-empty",
));
}
if source.is_empty() {
return Err(invalid_input(
"PythonFunctionDefinition source must be non-empty",
));
}
if python.is_empty() {
return Err(invalid_input(
"PythonFunctionDefinition python must be non-empty",
));
}
let mut seen = HashSet::with_capacity(packages.len());
for package in packages {
if package.is_empty() {
return Err(invalid_input(
"PythonFunctionDefinition package must be non-empty",
));
}
if !seen.insert(package.as_str()) {
return Err(invalid_input(
"PythonFunctionDefinition packages must not contain duplicates",
));
}
}
Ok(())
}
/// Explicit capability grant attached to a [`FunctionDefinition`].
///
/// Secret references are carried on the trusted registration wire but are
/// omitted from [`Debug`] output. Plaintext secret values are never part of
/// this type.
#[derive(Clone, PartialEq, Eq)]
pub struct FunctionCapability {
kind: FunctionCapabilityKind,
}
#[derive(Clone, PartialEq, Eq)]
enum FunctionCapabilityKind {
Network {
origin: String,
},
Secret {
reference: String,
environment_variable: String,
},
}
impl FunctionCapability {
/// Create a network capability for a non-empty origin.
pub fn try_network(origin: impl Into<String>) -> Result<Self> {
let origin = origin.into();
if origin.is_empty() {
return Err(invalid_input(
"FunctionCapability network origin must be non-empty",
));
}
Ok(Self {
kind: FunctionCapabilityKind::Network { origin },
})
}
/// Create a secret capability for a non-empty reference and environment variable.
///
/// Errors name the fields and never echo the reference value.
pub fn try_secret(
reference: impl Into<String>,
environment_variable: impl Into<String>,
) -> Result<Self> {
let reference = reference.into();
let environment_variable = environment_variable.into();
if reference.is_empty() {
return Err(invalid_input(
"FunctionCapability secret reference must be non-empty",
));
}
if environment_variable.is_empty() {
return Err(invalid_input(
"FunctionCapability secret environment_variable must be non-empty",
));
}
Ok(Self {
kind: FunctionCapabilityKind::Secret {
reference,
environment_variable,
},
})
}
/// Network origin when this capability is a network grant.
pub fn origin(&self) -> Option<&str> {
match &self.kind {
FunctionCapabilityKind::Network { origin } => Some(origin.as_str()),
FunctionCapabilityKind::Secret { .. } => None,
}
}
/// Secret reference when this capability is a secret grant.
pub fn reference(&self) -> Option<&str> {
match &self.kind {
FunctionCapabilityKind::Secret { reference, .. } => Some(reference.as_str()),
FunctionCapabilityKind::Network { .. } => None,
}
}
/// Environment variable name when this capability is a secret grant.
pub fn environment_variable(&self) -> Option<&str> {
match &self.kind {
FunctionCapabilityKind::Secret {
environment_variable,
..
} => Some(environment_variable.as_str()),
FunctionCapabilityKind::Network { .. } => None,
}
}
fn to_wire(&self) -> CapabilityWire {
match &self.kind {
FunctionCapabilityKind::Network { origin } => CapabilityWire::Network {
origin: origin.clone(),
},
FunctionCapabilityKind::Secret {
reference,
environment_variable,
} => CapabilityWire::Secret {
reference: reference.clone(),
environment_variable: environment_variable.clone(),
},
}
}
fn from_wire(wire: CapabilityWire) -> Result<Self> {
match wire {
CapabilityWire::Network { origin } => Self::try_network(origin),
CapabilityWire::Secret {
reference,
environment_variable,
} => Self::try_secret(reference, environment_variable),
}
}
}
impl fmt::Debug for FunctionCapability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
FunctionCapabilityKind::Network { origin } => f
.debug_struct("FunctionCapability")
.field("kind", &"network")
.field("origin", origin)
.finish(),
FunctionCapabilityKind::Secret {
environment_variable,
..
} => f
.debug_struct("FunctionCapability")
.field("kind", &"secret")
.field("reference", &"<redacted>")
.field("environment_variable", environment_variable)
.finish(),
}
}
}
/// Immutable registration input for a first-class Function (format version 1).
///
/// This value has no catalog identity. Source bodies and secret references are
/// present on the trusted serde wire but omitted from [`Debug`].
#[derive(Clone, PartialEq, Eq)]
pub struct FunctionDefinition {
signature: FunctionSignature,
python_definition: PythonFunctionDefinition,
capabilities: Vec<FunctionCapability>,
}
impl FunctionDefinition {
/// Create a definition from a signature, Python implementation, and capabilities.
///
/// Emptiness and package uniqueness are enforced by the child constructors.
pub fn try_new(
signature: FunctionSignature,
python_definition: PythonFunctionDefinition,
capabilities: Vec<FunctionCapability>,
) -> Result<Self> {
Ok(Self {
signature,
python_definition,
capabilities,
})
}
/// Function signature.
pub fn signature(&self) -> &FunctionSignature {
&self.signature
}
/// Python implementation description.
pub fn python_definition(&self) -> &PythonFunctionDefinition {
&self.python_definition
}
/// Ordered capability grants.
pub fn capabilities(&self) -> &[FunctionCapability] {
&self.capabilities
}
fn to_wire(&self) -> Result<FunctionDefinitionWire> {
Ok(FunctionDefinitionWire {
format_version: FORMAT_VERSION_V1,
signature: self.signature.to_wire()?,
implementation: ImplementationWire::Python {
module: self.python_definition.module.clone(),
callable: self.python_definition.callable.clone(),
source: self.python_definition.source.clone(),
python: self.python_definition.python.clone(),
packages: self.python_definition.packages.clone(),
},
capabilities: self
.capabilities
.iter()
.map(FunctionCapability::to_wire)
.collect(),
})
}
fn from_wire(wire: FunctionDefinitionWire) -> Result<Self> {
if wire.format_version != FORMAT_VERSION_V1 {
return Err(invalid_input(format!(
"unsupported FunctionDefinition format_version {}",
wire.format_version
)));
}
let signature = FunctionSignature::from_wire(wire.signature)?;
let python_definition = match wire.implementation {
ImplementationWire::Python {
module,
callable,
source,
python,
packages,
} => PythonFunctionDefinition::try_new(module, callable, source, python, packages)?,
};
let capabilities = wire
.capabilities
.into_iter()
.map(FunctionCapability::from_wire)
.collect::<Result<Vec<_>>>()?;
Self::try_new(signature, python_definition, capabilities)
}
}
impl fmt::Debug for FunctionDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FunctionDefinition")
.field("signature", &self.signature)
.field("python_definition", &self.python_definition)
.field("capabilities", &self.capabilities)
.finish()
}
}
// Do not derive Debug: wire payloads carry Python source and secret references.
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct FunctionDefinitionWire {
format_version: u32,
signature: SignatureWire,
implementation: ImplementationWire,
capabilities: Vec<CapabilityWire>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "kind", deny_unknown_fields)]
enum ImplementationWire {
#[serde(rename = "python")]
Python {
module: String,
callable: String,
source: String,
python: String,
packages: Vec<String>,
},
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "kind", deny_unknown_fields)]
enum CapabilityWire {
#[serde(rename = "network")]
Network { origin: String },
#[serde(rename = "secret")]
Secret {
reference: String,
environment_variable: String,
},
}
impl Serialize for FunctionDefinition {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_wire()
.map_err(serde::ser::Error::custom)?
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for FunctionDefinition {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = FunctionDefinitionWire::deserialize(deserializer)?;
Self::from_wire(wire).map_err(D::Error::custom)
}
}
@@ -0,0 +1,837 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
//! Contract tests for FunctionDefinition registration input (FF-007 / B1c).
//!
//! These tests pin the intended public surface under [`lancedb::function`] for
//! Python definition transport only. They intentionally fail to compile until
//! that API exists.
//!
//! Rejection cases are judged by `Result` structure (`is_err` / `is_ok`), never
//! by diagnostic message substrings.
use std::collections::BTreeSet;
use arrow_schema::DataType;
use lancedb::Result;
use lancedb::function::{
Function, FunctionCapability, FunctionDefinition, FunctionId, FunctionOutput,
FunctionParameter, FunctionSignature, PythonFunctionDefinition,
};
use serde_json::Value;
fn sample_signature() -> Result<FunctionSignature> {
FunctionSignature::try_new(
vec![
FunctionParameter::new("text", DataType::Utf8),
FunctionParameter::new("limit", DataType::Int32),
],
FunctionOutput::new(DataType::Utf8, true),
)
}
fn sample_source() -> &'static str {
"def normalize(text, limit):\n return text[:limit]\n"
}
fn sample_python_definition() -> Result<PythonFunctionDefinition> {
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
sample_source(),
"3.12",
vec!["Unidecode==1.3.8".to_string()],
)
}
fn sample_capabilities() -> Result<Vec<FunctionCapability>> {
Ok(vec![
FunctionCapability::try_network("https://api.example.com")?,
FunctionCapability::try_secret("secret://team/api-token", "API_TOKEN")?,
])
}
fn sample_definition() -> Result<FunctionDefinition> {
FunctionDefinition::try_new(
sample_signature()?,
sample_python_definition()?,
sample_capabilities()?,
)
}
fn assert_json_object_keys_exact(value: &Value, expected: &[&str]) {
let object = value
.as_object()
.unwrap_or_else(|| panic!("expected JSON object, got {value}"));
let keys: BTreeSet<&str> = object.keys().map(|k| k.as_str()).collect();
let expected: BTreeSet<&str> = expected.iter().copied().collect();
assert_eq!(
keys, expected,
"JSON object key set must match exactly (iteration order is not a contract); got {keys:?}, expected {expected:?} in {value}"
);
}
fn assert_json_object_keys_subset(value: &Value, allowed: &[&str]) {
let object = value
.as_object()
.unwrap_or_else(|| panic!("expected JSON object, got {value}"));
for key in object.keys() {
assert!(
allowed.contains(&key.as_str()),
"unexpected JSON key `{key}` in {value}"
);
}
}
/// Identity / lineage / artifact / runtime fields that must not appear as public
/// object keys on FunctionDefinition wire. Parameter object key `name` is not
/// listed here: FunctionSignature legitimately uses it under `signature.parameters`.
const FORBIDDEN_DEFINITION_KEYS: &[&str] = &[
"id",
"function_id",
"FunctionId",
"catalog",
"catalog_name",
"version",
"function_version",
"FunctionVersion",
"lineage",
"user_version",
"idempotency_key",
"digest",
"artifact",
"artifact_digest",
"storage",
"storage_location",
"location",
"deterministic",
"null_policy",
"nullPolicy",
"timestamp",
"created_at",
"updated_at",
"worker",
"scheduler",
"attempt",
"attempt_id",
"replica",
"placement",
];
fn assert_object_keys_not_forbidden(value: &Value, context: &str) {
let object = value.as_object().unwrap_or_else(|| {
panic!("expected JSON object at {context}, got {value}");
});
for key in object.keys() {
assert!(
!FORBIDDEN_DEFINITION_KEYS.contains(&key.as_str()),
"FunctionDefinition wire must not contain forbidden key `{key}` at {context}: {value}"
);
}
}
fn assert_forbidden_definition_keys_absent(value: &Value) {
// Top-level definition key set (order-independent).
assert_json_object_keys_exact(
value,
&[
"format_version",
"signature",
"implementation",
"capabilities",
],
);
assert_object_keys_not_forbidden(value, "definition");
// Catalog / function identity name is absent at definition root; parameter
// `name` is allowed only under signature.parameters.
assert!(
value.get("name").is_none(),
"top-level FunctionDefinition wire must not contain catalog/function identity key `name`: {value}"
);
assert!(
value.get("catalog_name").is_none(),
"top-level FunctionDefinition wire must not contain `catalog_name`: {value}"
);
let implementation = value.get("implementation").expect("implementation object");
assert_json_object_keys_exact(
implementation,
&["kind", "module", "callable", "source", "python", "packages"],
);
assert_object_keys_not_forbidden(implementation, "implementation");
assert!(
implementation.get("name").is_none(),
"implementation must not contain catalog/function identity key `name`: {implementation}"
);
assert!(
implementation.get("catalog_name").is_none(),
"implementation must not contain `catalog_name`: {implementation}"
);
let capabilities = value
.get("capabilities")
.and_then(Value::as_array)
.expect("capabilities array");
for (idx, capability) in capabilities.iter().enumerate() {
let kind = capability
.get("kind")
.and_then(Value::as_str)
.unwrap_or_else(|| panic!("capabilities[{idx}] missing kind"));
match kind {
"network" => assert_json_object_keys_exact(capability, &["kind", "origin"]),
"secret" => assert_json_object_keys_exact(
capability,
&["kind", "reference", "environment_variable"],
),
other => panic!("unexpected capability kind `{other}` in contract fixture"),
}
let context = format!("capabilities[{idx}]");
assert_object_keys_not_forbidden(capability, &context);
assert!(
capability.get("name").is_none(),
"{context} must not contain catalog/function identity key `name`: {capability}"
);
assert!(
capability.get("catalog_name").is_none(),
"{context} must not contain `catalog_name`: {capability}"
);
}
// Signature may carry parameter objects with key `name`. Still reject
// identity/lineage/runtime keys and function-identity `name`/`catalog_name`
// on the signature and output objects themselves.
let signature = value.get("signature").expect("signature object");
assert_object_keys_not_forbidden(signature, "signature");
assert!(
signature.get("name").is_none(),
"signature object must not contain catalog/function identity key `name`: {signature}"
);
assert!(
signature.get("catalog_name").is_none(),
"signature object must not contain `catalog_name`: {signature}"
);
if let Some(parameters) = signature.get("parameters").and_then(Value::as_array) {
for (idx, parameter) in parameters.iter().enumerate() {
let context = format!("signature.parameters[{idx}]");
assert_object_keys_not_forbidden(parameter, &context);
assert!(
parameter.get("catalog_name").is_none(),
"{context} must not contain `catalog_name`: {parameter}"
);
// `name` is intentionally allowed on parameter objects.
assert!(
parameter.get("name").is_some(),
"{context} must include parameter `name`"
);
}
}
if let Some(output) = signature.get("output") {
assert_object_keys_not_forbidden(output, "signature.output");
assert!(
output.get("name").is_none(),
"signature.output must not contain catalog/function identity key `name`: {output}"
);
assert!(
output.get("catalog_name").is_none(),
"signature.output must not contain `catalog_name`: {output}"
);
}
}
#[test]
fn definition_json_round_trip_pins_exact_wire_shape_and_order() -> Result<()> {
let definition = sample_definition()?;
assert_eq!(definition.signature().parameters().len(), 2);
assert_eq!(definition.signature().parameters()[0].name(), "text");
assert_eq!(
definition.signature().parameters()[0].data_type(),
&DataType::Utf8
);
assert_eq!(definition.signature().parameters()[1].name(), "limit");
assert_eq!(
definition.signature().parameters()[1].data_type(),
&DataType::Int32
);
assert_eq!(definition.signature().output().data_type(), &DataType::Utf8);
assert!(definition.signature().output().nullable());
let python = definition.python_definition();
assert_eq!(python.module(), "normalize_mod");
assert_eq!(python.callable(), "normalize");
assert_eq!(python.source(), sample_source());
assert_eq!(python.python(), "3.12");
assert_eq!(python.packages(), &["Unidecode==1.3.8".to_string()]);
let capabilities = definition.capabilities();
assert_eq!(capabilities.len(), 2);
assert_eq!(capabilities[0].origin(), Some("https://api.example.com"));
assert_eq!(capabilities[0].reference(), None);
assert_eq!(capabilities[0].environment_variable(), None);
assert_eq!(capabilities[1].reference(), Some("secret://team/api-token"));
assert_eq!(capabilities[1].environment_variable(), Some("API_TOKEN"));
assert_eq!(capabilities[1].origin(), None);
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
assert_json_object_keys_exact(
&json,
&[
"format_version",
"signature",
"implementation",
"capabilities",
],
);
assert_eq!(json["format_version"], 1);
let signature = json
.get("signature")
.and_then(Value::as_object)
.expect("signature object");
assert_json_object_keys_subset(&Value::Object(signature.clone()), &["parameters", "output"]);
let parameters = signature
.get("parameters")
.and_then(Value::as_array)
.expect("parameters array");
assert_eq!(parameters.len(), 2);
assert_eq!(parameters[0]["name"], Value::String("text".into()));
assert_eq!(parameters[1]["name"], Value::String("limit".into()));
for parameter in parameters {
assert_json_object_keys_subset(parameter, &["name", "data_type_ipc"]);
assert!(
parameter
.get("data_type_ipc")
.and_then(Value::as_str)
.is_some_and(|s| !s.is_empty()),
"parameter data_type_ipc must be non-empty base64"
);
}
let output = signature
.get("output")
.and_then(Value::as_object)
.expect("output object");
assert_json_object_keys_subset(
&Value::Object(output.clone()),
&["data_type_ipc", "nullable"],
);
assert_eq!(output.get("nullable"), Some(&Value::Bool(true)));
let implementation = json.get("implementation").expect("implementation object");
assert_json_object_keys_exact(
implementation,
&["kind", "module", "callable", "source", "python", "packages"],
);
assert_eq!(implementation["kind"], Value::String("python".into()));
assert_eq!(
implementation["module"],
Value::String("normalize_mod".into())
);
assert_eq!(
implementation["callable"],
Value::String("normalize".into())
);
assert_eq!(
implementation["source"],
Value::String(sample_source().into())
);
assert_eq!(implementation["python"], Value::String("3.12".into()));
assert_eq!(
implementation["packages"],
Value::Array(vec![Value::String("Unidecode==1.3.8".into())])
);
let capabilities_json = json
.get("capabilities")
.and_then(Value::as_array)
.expect("capabilities array");
assert_eq!(capabilities_json.len(), 2);
assert_json_object_keys_exact(&capabilities_json[0], &["kind", "origin"]);
assert_eq!(
capabilities_json[0]["kind"],
Value::String("network".into())
);
assert_eq!(
capabilities_json[0]["origin"],
Value::String("https://api.example.com".into())
);
assert_json_object_keys_exact(
&capabilities_json[1],
&["kind", "reference", "environment_variable"],
);
assert_eq!(capabilities_json[1]["kind"], Value::String("secret".into()));
assert_eq!(
capabilities_json[1]["reference"],
Value::String("secret://team/api-token".into())
);
assert_eq!(
capabilities_json[1]["environment_variable"],
Value::String("API_TOKEN".into())
);
// Same ordered signature IPC representation as Function handle transport.
let function = Function::new(FunctionId::try_new("fn.wire.compare")?, sample_signature()?);
let function_json = serde_json::to_value(&function).expect("serialize Function");
assert_eq!(json["signature"], function_json["signature"]);
let restored: FunctionDefinition =
serde_json::from_value(json.clone()).expect("deserialize FunctionDefinition");
assert_eq!(
restored.signature().parameters()[0].name(),
definition.signature().parameters()[0].name()
);
assert_eq!(
restored.signature().parameters()[0].data_type(),
definition.signature().parameters()[0].data_type()
);
assert_eq!(
restored.signature().parameters()[1].name(),
definition.signature().parameters()[1].name()
);
assert_eq!(
restored.signature().parameters()[1].data_type(),
definition.signature().parameters()[1].data_type()
);
assert_eq!(
restored.signature().output().data_type(),
definition.signature().output().data_type()
);
assert_eq!(
restored.signature().output().nullable(),
definition.signature().output().nullable()
);
assert_eq!(
restored.python_definition().module(),
definition.python_definition().module()
);
assert_eq!(
restored.python_definition().callable(),
definition.python_definition().callable()
);
assert_eq!(
restored.python_definition().source(),
definition.python_definition().source()
);
assert_eq!(
restored.python_definition().python(),
definition.python_definition().python()
);
assert_eq!(
restored.python_definition().packages(),
definition.python_definition().packages()
);
assert_eq!(restored.capabilities().len(), 2);
assert_eq!(
restored.capabilities()[0].origin(),
definition.capabilities()[0].origin()
);
assert_eq!(
restored.capabilities()[1].reference(),
definition.capabilities()[1].reference()
);
assert_eq!(
restored.capabilities()[1].environment_variable(),
definition.capabilities()[1].environment_variable()
);
// Package and capability order are part of the structural wire.
let multi_pkg = FunctionDefinition::try_new(
sample_signature()?,
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
sample_source(),
"3.12",
vec![
"Unidecode==1.3.8".to_string(),
"requests==2.32.3".to_string(),
],
)?,
vec![
FunctionCapability::try_secret("secret://team/api-token", "API_TOKEN")?,
FunctionCapability::try_network("https://api.example.com")?,
FunctionCapability::try_network("https://other.example.com")?,
],
)?;
let multi_json = serde_json::to_value(&multi_pkg).expect("serialize multi-order definition");
assert_eq!(
multi_json["implementation"]["packages"],
Value::Array(vec![
Value::String("Unidecode==1.3.8".into()),
Value::String("requests==2.32.3".into()),
])
);
assert_eq!(
multi_json["capabilities"][0]["kind"],
Value::String("secret".into())
);
assert_eq!(
multi_json["capabilities"][1]["origin"],
Value::String("https://api.example.com".into())
);
assert_eq!(
multi_json["capabilities"][2]["origin"],
Value::String("https://other.example.com".into())
);
let multi_restored: FunctionDefinition =
serde_json::from_value(multi_json.clone()).expect("deserialize multi-order definition");
assert_eq!(
multi_restored.python_definition().packages(),
&[
"Unidecode==1.3.8".to_string(),
"requests==2.32.3".to_string()
]
);
assert_eq!(
multi_restored.capabilities()[0].reference(),
Some("secret://team/api-token")
);
assert_eq!(
multi_restored.capabilities()[1].origin(),
Some("https://api.example.com")
);
assert_eq!(
multi_restored.capabilities()[2].origin(),
Some("https://other.example.com")
);
// Byte-for-byte repeated serde_json encoding for the same value.
let encoded_a = serde_json::to_string(&definition).expect("encode a");
let encoded_b = serde_json::to_string(&definition).expect("encode b");
assert_eq!(encoded_a, encoded_b);
assert_eq!(
serde_json::to_value(&restored).expect("re-serialize restored"),
json
);
assert_eq!(
serde_json::to_string(&multi_restored).expect("re-encode multi"),
serde_json::to_string(&multi_pkg).expect("encode multi")
);
Ok(())
}
#[test]
fn definition_wire_excludes_identity_lineage_artifact_and_runtime_fields() -> Result<()> {
let definition = sample_definition()?;
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
// Forbidden public fields are object keys at structural levels only.
// Do not substring-scan encoded JSON: user source/reference/package text
// may legitimately contain those tokens.
assert_forbidden_definition_keys_absent(&json);
Ok(())
}
#[test]
fn definition_decode_fails_closed_for_unknown_version_fields_and_kinds() -> Result<()> {
let definition = sample_definition()?;
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
let mut unknown_version = json.clone();
unknown_version["format_version"] = Value::from(2);
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_version).is_err(),
"format_version other than 1 must fail closed"
);
let mut unknown_outer = json.clone();
unknown_outer
.as_object_mut()
.unwrap()
.insert("unexpected_field".into(), Value::Bool(true));
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_outer).is_err(),
"unknown outer field must fail closed"
);
let mut unknown_implementation_field = json.clone();
unknown_implementation_field["implementation"]
.as_object_mut()
.unwrap()
.insert("entrypoint".into(), Value::String("main".into()));
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_implementation_field).is_err(),
"unknown nested implementation field must fail closed"
);
let mut unknown_capability_field = json.clone();
unknown_capability_field["capabilities"][0]
.as_object_mut()
.unwrap()
.insert("headers".into(), Value::Object(Default::default()));
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_capability_field).is_err(),
"unknown nested capability field must fail closed"
);
let mut unknown_implementation_kind = json.clone();
unknown_implementation_kind["implementation"]["kind"] = Value::String("builtin".into());
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_implementation_kind).is_err(),
"unknown implementation kind must fail closed"
);
let mut unknown_capability_kind = json.clone();
unknown_capability_kind["capabilities"][0]["kind"] = Value::String("filesystem".into());
assert!(
serde_json::from_value::<FunctionDefinition>(unknown_capability_kind).is_err(),
"unknown capability kind must fail closed"
);
Ok(())
}
#[test]
fn constructors_and_decode_reject_empty_fields_and_duplicate_packages() -> Result<()> {
let signature = sample_signature()?;
let packages = vec!["Unidecode==1.3.8".to_string()];
let capabilities = sample_capabilities()?;
assert!(
PythonFunctionDefinition::try_new(
"",
"normalize",
sample_source(),
"3.12",
packages.clone()
)
.is_err(),
"empty module must be rejected"
);
assert!(
PythonFunctionDefinition::try_new(
"normalize_mod",
"",
sample_source(),
"3.12",
packages.clone()
)
.is_err(),
"empty callable must be rejected"
);
assert!(
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
"",
"3.12",
packages.clone()
)
.is_err(),
"empty source must be rejected"
);
assert!(
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
sample_source(),
"",
packages.clone()
)
.is_err(),
"empty python runtime request must be rejected"
);
assert!(
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
sample_source(),
"3.12",
vec!["".to_string()],
)
.is_err(),
"empty package requirement must be rejected"
);
assert!(
PythonFunctionDefinition::try_new(
"normalize_mod",
"normalize",
sample_source(),
"3.12",
vec![
"Unidecode==1.3.8".to_string(),
"Unidecode==1.3.8".to_string(),
],
)
.is_err(),
"duplicate package requirements must be rejected"
);
assert!(
FunctionCapability::try_network("").is_err(),
"empty network origin must be rejected"
);
assert!(
FunctionCapability::try_secret("", "API_TOKEN").is_err(),
"empty secret reference must be rejected"
);
assert!(
FunctionCapability::try_secret("secret://team/api-token", "").is_err(),
"empty secret environment variable must be rejected"
);
// Decode path must enforce the same emptiness / uniqueness rules.
let definition = FunctionDefinition::try_new(
signature.clone(),
sample_python_definition()?,
capabilities.clone(),
)?;
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
for (pointer, empty) in [
("/implementation/module", ""),
("/implementation/callable", ""),
("/implementation/source", ""),
("/implementation/python", ""),
("/capabilities/0/origin", ""),
("/capabilities/1/reference", ""),
("/capabilities/1/environment_variable", ""),
] {
let mut invalid = json.clone();
let target = invalid
.pointer_mut(pointer)
.unwrap_or_else(|| panic!("missing pointer {pointer}"));
*target = Value::String(empty.into());
assert!(
serde_json::from_value::<FunctionDefinition>(invalid).is_err(),
"decode must reject empty value at {pointer}"
);
}
let mut empty_package = json.clone();
empty_package["implementation"]["packages"] = Value::Array(vec![Value::String("".into())]);
assert!(
serde_json::from_value::<FunctionDefinition>(empty_package).is_err(),
"decode must reject empty package requirement"
);
let mut duplicate_packages = json.clone();
duplicate_packages["implementation"]["packages"] = Value::Array(vec![
Value::String("Unidecode==1.3.8".into()),
Value::String("Unidecode==1.3.8".into()),
]);
assert!(
serde_json::from_value::<FunctionDefinition>(duplicate_packages).is_err(),
"decode must reject duplicate package requirements"
);
// Keep the constructor path for FunctionDefinition itself structurally valid
// when children are valid; emptiness is owned by child constructors above.
assert!(
FunctionDefinition::try_new(signature, sample_python_definition()?, capabilities).is_ok()
);
Ok(())
}
#[test]
fn secret_capability_wire_rejects_plaintext_value_fields() -> Result<()> {
let definition = sample_definition()?;
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
let secret = &json["capabilities"][1];
assert_json_object_keys_exact(secret, &["kind", "reference", "environment_variable"]);
assert!(secret.get("value").is_none());
assert!(secret.get("plaintext_secret").is_none());
let mut with_value = json.clone();
with_value["capabilities"][1]
.as_object_mut()
.unwrap()
.insert("value".into(), Value::String("super-secret".into()));
assert!(
serde_json::from_value::<FunctionDefinition>(with_value).is_err(),
"secret capability must reject `value`"
);
let mut with_plaintext = json.clone();
with_plaintext["capabilities"][1]
.as_object_mut()
.unwrap()
.insert(
"plaintext_secret".into(),
Value::String("super-secret".into()),
);
assert!(
serde_json::from_value::<FunctionDefinition>(with_plaintext).is_err(),
"secret capability must reject `plaintext_secret`"
);
Ok(())
}
#[test]
fn debug_redacts_source_and_secret_reference_while_getters_remain_exact() -> Result<()> {
let python = sample_python_definition()?;
let source = sample_source();
assert_eq!(python.source(), source);
let python_debug = format!("{python:?}");
assert!(
!python_debug.contains(source),
"PythonFunctionDefinition Debug must not contain source body: {python_debug}"
);
assert!(
!python_debug.contains("return text[:limit]"),
"PythonFunctionDefinition Debug must not leak source fragments: {python_debug}"
);
let secret = FunctionCapability::try_secret("secret://team/api-token", "API_TOKEN")?;
assert_eq!(secret.reference(), Some("secret://team/api-token"));
assert_eq!(secret.environment_variable(), Some("API_TOKEN"));
let secret_debug = format!("{secret:?}");
assert!(
!secret_debug.contains("secret://team/api-token"),
"FunctionCapability secret Debug must not contain reference: {secret_debug}"
);
let definition = sample_definition()?;
assert_eq!(definition.python_definition().source(), source);
assert_eq!(
definition.capabilities()[1].reference(),
Some("secret://team/api-token")
);
let definition_debug = format!("{definition:?}");
assert!(
!definition_debug.contains(source),
"FunctionDefinition Debug must not contain source body: {definition_debug}"
);
assert!(
!definition_debug.contains("secret://team/api-token"),
"FunctionDefinition Debug must not contain secret reference: {definition_debug}"
);
Ok(())
}
#[test]
fn definition_has_no_identity_before_registration_and_is_not_a_function_handle() -> Result<()> {
let definition = sample_definition()?;
let json = serde_json::to_value(&definition).expect("serialize FunctionDefinition");
assert!(json.get("id").is_none());
assert!(json.get("function_id").is_none());
assert_json_object_keys_exact(
&json,
&[
"format_version",
"signature",
"implementation",
"capabilities",
],
);
// Identity exists only on the immutable Function handle after registration.
// Definition remains a separate authoring value and does not borrow or mint an ID.
let registered = Function::new(
FunctionId::try_new("fn.published.after.registration")?,
definition.signature().clone(),
);
assert_eq!(registered.id().as_str(), "fn.published.after.registration");
assert_eq!(
registered.signature().parameters().len(),
definition.signature().parameters().len()
);
let definition_again = serde_json::to_value(&definition).expect("re-serialize definition");
assert!(definition_again.get("id").is_none());
assert!(definition_again.get("function_id").is_none());
assert_ne!(
serde_json::to_value(&registered).expect("serialize Function"),
definition_again,
"Function handle wire must remain distinct from FunctionDefinition wire"
);
Ok(())
}