use std::collections::BTreeMap;
use serde::de::{self, DeserializeOwned};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use crate::secrets::SecretBinding;
use crate::{Error, Result};
pub const FUNCTION_BLOB_V2_TYPE: &str = "blob_v2";
fn invalid_json(error: impl std::fmt::Display) -> Error {
Error::InvalidInput {
message: format!("invalid remote Function JSON: {error}"),
}
}
fn write_canonical_json(value: &Value, output: &mut String) -> serde_json::Result<()> {
match value {
Value::Object(map) => {
output.push('{');
let mut entries = map.iter().collect::<Vec<_>>();
entries.sort_unstable_by_key(|(key, _)| *key);
for (index, (key, value)) in entries.into_iter().enumerate() {
if index != 0 {
output.push(',');
}
output.push_str(&serde_json::to_string(key)?);
output.push(':');
write_canonical_json(value, output)?;
}
output.push('}');
}
Value::Array(values) => {
output.push('[');
for (index, value) in values.iter().enumerate() {
if index != 0 {
output.push(',');
}
write_canonical_json(value, output)?;
}
output.push(']');
}
other => output.push_str(&serde_json::to_string(other)?),
}
Ok(())
}
fn canonical_json<T: Serialize>(value: &T) -> Result<String> {
let value = serde_json::to_value(value).map_err(invalid_json)?;
let mut output = String::new();
write_canonical_json(&value, &mut output).map_err(invalid_json)?;
Ok(output)
}
fn from_json<T: DeserializeOwned>(json: &str) -> Result<T> {
serde_json::from_str(json).map_err(invalid_json)
}
fn validate_literal(value: &Value) -> Result<()> {
match value {
Value::Number(number) if number.is_f64() => Err(Error::InvalidInput {
message: "floating-point Function literals are not part of the Slice 1 canonical wire contract"
.to_string(),
}),
Value::Array(values) => values.iter().try_for_each(validate_literal),
Value::Object(values) => values.values().try_for_each(validate_literal),
_ => Ok(()),
}
}
fn has_unknown_keys(value: &Value, allowed: &[&str]) -> bool {
value
.as_object()
.is_some_and(|object| object.keys().any(|key| !allowed.contains(&key.as_str())))
}
pub(crate) const FUNCTION_VERSION_REF_FIELDS: &[&str] = &[
"name",
"namespace_path",
"object_id",
"location",
"version",
"manifest_digest",
];
fn application_has_unknown_nested_fields(value: &Value) -> bool {
let Some(application) = value.as_object() else {
return false;
};
if application
.get("function")
.is_some_and(|value| has_unknown_keys(value, FUNCTION_VERSION_REF_FIELDS))
{
return true;
}
if application
.get("inputs")
.and_then(Value::as_array)
.is_some_and(|inputs| {
inputs
.iter()
.any(|input| has_unknown_keys(input, &["parameter", "kind", "value"]))
})
{
return true;
}
application.get("output").is_some_and(|output| {
has_unknown_keys(output, &["kind", "arrow_type", "nullable", "fields"])
|| output
.get("fields")
.and_then(Value::as_array)
.is_some_and(|fields| {
fields
.iter()
.any(|field| has_unknown_keys(field, &["name", "arrow_type", "nullable"]))
})
})
}
macro_rules! impl_json {
($type:ty) => {
impl $type {
pub fn from_json(json: &str) -> Result<Self> {
from_json(json)
}
pub fn to_canonical_json(&self) -> Result<String> {
canonical_json(self)
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionArtifact {
pub kind: String,
pub digest: String,
pub entrypoint: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionParameter {
pub name: String,
pub arrow_type: String,
pub nullable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionResultField {
pub name: String,
pub arrow_type: String,
pub nullable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionOutput {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arrow_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nullable: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<FunctionResultField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionSignature {
pub inputs: Vec<FunctionParameter>,
pub output: FunctionOutput,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initialization: Vec<FunctionParameter>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PythonEnvironmentSpec {
pub kind: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PythonRuntimeSpec {
Python {
python_version: String,
environment: PythonEnvironmentSpec,
env: BTreeMap<String, String>,
},
PythonV2 {
python_version: String,
environment: PythonEnvironmentSpec,
env: BTreeMap<String, String>,
},
Unrecognized { kind: String },
}
impl PythonRuntimeSpec {
pub fn kind(&self) -> &str {
match self {
Self::Python { .. } => "python",
Self::PythonV2 { .. } => "python_v2",
Self::Unrecognized { kind } => kind,
}
}
pub fn python_version(&self) -> Option<&str> {
match self {
Self::Python { python_version, .. } | Self::PythonV2 { python_version, .. } => {
Some(python_version)
}
Self::Unrecognized { .. } => None,
}
}
pub fn environment(&self) -> Option<&PythonEnvironmentSpec> {
match self {
Self::Python { environment, .. } | Self::PythonV2 { environment, .. } => {
Some(environment)
}
Self::Unrecognized { .. } => None,
}
}
pub fn env(&self) -> Option<&BTreeMap<String, String>> {
match self {
Self::Python { env, .. } | Self::PythonV2 { env, .. } => Some(env),
Self::Unrecognized { .. } => None,
}
}
pub fn requires_gpu(&self) -> bool {
matches!(self, Self::PythonV2 { .. })
}
}
#[derive(Deserialize)]
struct PythonRuntimeV1Wire {
python_version: String,
environment: PythonEnvironmentSpec,
#[serde(default)]
env: BTreeMap<String, String>,
#[serde(default)]
gpu: Option<Value>,
}
#[derive(Deserialize)]
struct PythonRuntimeV2Wire {
python_version: String,
environment: PythonEnvironmentSpec,
#[serde(default)]
env: BTreeMap<String, String>,
gpu: bool,
}
impl<'de> Deserialize<'de> for PythonRuntimeSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
let kind = value
.get("kind")
.ok_or_else(|| de::Error::missing_field("kind"))?
.as_str()
.ok_or_else(|| de::Error::custom("runtime.kind must be a string"))?
.to_string();
match kind.as_str() {
"python" => {
let wire: PythonRuntimeV1Wire =
serde_json::from_value(value).map_err(de::Error::custom)?;
if wire.gpu.is_some() {
return Err(de::Error::custom(
"python runtime with gpu requires kind='python_v2'",
));
}
Ok(Self::Python {
python_version: wire.python_version,
environment: wire.environment,
env: wire.env,
})
}
"python_v2" => {
let wire: PythonRuntimeV2Wire =
serde_json::from_value(value).map_err(de::Error::custom)?;
if !wire.gpu {
return Err(de::Error::custom("runtime.gpu must be true"));
}
Ok(Self::PythonV2 {
python_version: wire.python_version,
environment: wire.environment,
env: wire.env,
})
}
_ => Ok(Self::Unrecognized { kind }),
}
}
}
impl Serialize for PythonRuntimeSpec {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct PythonRuntimeRef<'a> {
kind: &'static str,
python_version: &'a str,
environment: &'a PythonEnvironmentSpec,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
env: &'a BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
gpu: Option<bool>,
}
#[derive(Serialize)]
struct UnrecognizedRuntimeRef<'a> {
kind: &'a str,
}
match self {
Self::Python {
python_version,
environment,
env,
} => PythonRuntimeRef {
kind: "python",
python_version,
environment,
env,
gpu: None,
}
.serialize(serializer),
Self::PythonV2 {
python_version,
environment,
env,
} => PythonRuntimeRef {
kind: "python_v2",
python_version,
environment,
env,
gpu: Some(true),
}
.serialize(serializer),
Self::Unrecognized { kind } => UnrecognizedRuntimeRef { kind }.serialize(serializer),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionImage {
pub manifest_digest: String,
pub descriptor: Value,
pub source: bool,
}
fn deserialize_object_version<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<String, D::Error> {
let value = String::deserialize(deserializer)?;
match value.parse::<u64>() {
Ok(number) if number > 0 && number.to_string() == value => Ok(value),
_ => Err(de::Error::custom(
"Function version must be a canonical positive uint64",
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionVersion {
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
namespace_path: Vec<String>,
object_id: String,
location: String,
#[serde(deserialize_with = "deserialize_object_version")]
version: String,
image: FunctionImage,
signature: FunctionSignature,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
secret_bindings: Vec<SecretBinding>,
created_at: String,
metadata: BTreeMap<String, String>,
disabled: bool,
}
impl FunctionVersion {
pub fn object_id(&self) -> &str {
&self.object_id
}
pub fn location(&self) -> &str {
&self.location
}
pub fn metadata(&self) -> &BTreeMap<String, String> {
&self.metadata
}
pub fn disabled(&self) -> bool {
self.disabled
}
pub fn reference(&self) -> FunctionVersionRef {
FunctionVersionRef {
name: self.name.clone(),
namespace_path: self.namespace_path.clone(),
object_id: self.object_id.clone(),
location: self.location.clone(),
version: self.version.clone(),
manifest_digest: self.image.manifest_digest.clone(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn namespace_path(&self) -> &[String] {
&self.namespace_path
}
pub fn version(&self) -> &str {
&self.version
}
pub fn image(&self) -> &FunctionImage {
&self.image
}
pub fn signature(&self) -> &FunctionSignature {
&self.signature
}
pub fn secret_bindings(&self) -> &[SecretBinding] {
&self.secret_bindings
}
pub fn created_at(&self) -> &str {
&self.created_at
}
}
impl_json!(FunctionVersion);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionArtifactContent {
pub encoding: String,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PythonAdapterSpec {
pub kind: String,
pub version: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionArtifactRequest {
pub kind: String,
pub digest: String,
pub entrypoint: String,
pub content: FunctionArtifactContent,
pub adapter: PythonAdapterSpec,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionRegistrationRequest {
pub name: String,
pub artifact: FunctionArtifactRequest,
pub signature: FunctionSignature,
pub runtime: PythonRuntimeSpec,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secret_bindings: Vec<SecretBinding>,
}
impl_json!(FunctionRegistrationRequest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionVersionRef {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub namespace_path: Vec<String>,
pub object_id: String,
pub location: String,
#[serde(deserialize_with = "deserialize_object_version")]
pub version: String,
pub manifest_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApplicationInput {
pub parameter: String,
pub kind: String,
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionApplication {
function: FunctionVersionRef,
inputs: Vec<ApplicationInput>,
output: FunctionOutput,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
columns: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
initialization: BTreeMap<String, Value>,
#[serde(default, flatten, skip_serializing)]
unknown_fields: BTreeMap<String, Value>,
#[serde(default, skip)]
unknown_nested_fields: bool,
}
impl FunctionApplication {
pub fn function(&self) -> &FunctionVersionRef {
&self.function
}
pub fn inputs(&self) -> &[ApplicationInput] {
&self.inputs
}
pub fn output(&self) -> &FunctionOutput {
&self.output
}
pub fn columns(&self) -> &BTreeMap<String, String> {
&self.columns
}
pub fn initialization(&self) -> &BTreeMap<String, Value> {
&self.initialization
}
pub fn has_unknown_fields(&self) -> bool {
!self.unknown_fields.is_empty() || self.unknown_nested_fields
}
pub fn from_json(json: &str) -> Result<Self> {
let value: Value = from_json(json)?;
let has_unknown_nested_fields = application_has_unknown_nested_fields(&value);
let mut application: Self = serde_json::from_value(value).map_err(invalid_json)?;
application.unknown_nested_fields = has_unknown_nested_fields;
application
.inputs
.iter()
.try_for_each(|input| validate_literal(&input.value))?;
Ok(application)
}
pub fn to_canonical_json(&self) -> Result<String> {
self.inputs
.iter()
.try_for_each(|input| validate_literal(&input.value))?;
canonical_json(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputBinding {
pub parameter: String,
pub field_id: i32,
pub field_path: String,
pub arrow_type: String,
pub nullable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputMapping {
pub result_field: String,
pub output_name: String,
pub output_field_id: i32,
pub output_ordinal: u32,
pub arrow_type: String,
pub nullable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssignmentMapping {
pub output_name: String,
pub output_field_id: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionBinding {
binding_id: String,
function: FunctionVersionRef,
inputs: Vec<InputBinding>,
outputs: Vec<OutputMapping>,
#[serde(default, skip_serializing_if = "Option::is_none")]
assignment: Option<AssignmentMapping>,
#[serde(default, skip_serializing_if = "Option::is_none")]
input_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
output_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
initialization: Option<String>,
}
impl FunctionBinding {
pub fn binding_id(&self) -> &str {
&self.binding_id
}
pub fn function(&self) -> &FunctionVersionRef {
&self.function
}
pub fn inputs(&self) -> &[InputBinding] {
&self.inputs
}
pub fn outputs(&self) -> &[OutputMapping] {
&self.outputs
}
pub fn assignment(&self) -> Option<&AssignmentMapping> {
self.assignment.as_ref()
}
pub fn input_schema(&self) -> Option<&Value> {
self.input_schema.as_ref()
}
pub fn output_schema(&self) -> Option<&Value> {
self.output_schema.as_ref()
}
pub fn initialization(&self) -> Option<&str> {
self.initialization.as_deref()
}
}
impl_json!(FunctionBinding);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RefreshColumnResult {
pub rows_assigned: u64,
pub rows_failed: u64,
pub rows_remaining: u64,
pub source_version: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_version: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FunctionErrorsRequest {
pub job_id: Option<String>,
pub column: Option<String>,
pub limit: Option<usize>,
}
impl FunctionErrorsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn job_id(mut self, job_id: impl Into<String>) -> Self {
self.job_id = Some(job_id.into());
self
}
pub fn column(mut self, column: impl Into<String>) -> Self {
self.column = Some(column.into());
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionErrorRecord {
pub job_id: String,
pub fragment_id: u64,
#[serde(default)]
pub row_offset: Option<u32>,
pub column: String,
pub function: String,
pub function_version: String,
pub table_version: u64,
pub error_type: String,
pub error_message: String,
pub created_at_millis: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionErrorFragment {
pub job_id: String,
pub fragment_id: u64,
pub rows_skipped: u64,
pub rows_recorded: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionErrors {
pub records: Vec<FunctionErrorRecord>,
#[serde(default)]
pub fragments: Vec<FunctionErrorFragment>,
#[serde(default)]
pub truncated: bool,
}
impl RefreshColumnResult {
pub fn rows_filled(&self) -> u64 {
self.rows_assigned
}
pub fn version(&self) -> Option<u64> {
self.published_version
}
}
impl_json!(RefreshColumnResult);
#[cfg(test)]
mod conda_environment_tests {
use super::{PythonEnvironmentSpec, PythonRuntimeSpec};
#[test]
fn conda_channels_round_trip_and_pip_stays_bare() {
let conda: PythonEnvironmentSpec = serde_json::from_str(
r#"{"kind":"conda","packages":["numpy"],"channels":["conda-forge"]}"#,
)
.unwrap();
assert_eq!(conda.channels, ["conda-forge"]);
assert!(
serde_json::to_string(&conda)
.unwrap()
.contains(r#""channels":["conda-forge"]"#)
);
let pip: PythonEnvironmentSpec =
serde_json::from_str(r#"{"kind":"pip","packages":["numpy"]}"#).unwrap();
assert!(!serde_json::to_string(&pip).unwrap().contains("channels"));
}
#[test]
fn gpu_python_runtime_marker_round_trips_and_validates() {
let runtime: PythonRuntimeSpec = serde_json::from_str(
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"},"gpu":true}"#,
)
.unwrap();
assert_eq!(runtime.kind(), "python_v2");
assert!(runtime.requires_gpu());
assert_eq!(
super::canonical_json(&runtime).unwrap(),
r#"{"environment":{"kind":"pip"},"gpu":true,"kind":"python_v2","python_version":"3.12"}"#
);
for invalid in [
r#"{"kind":"python","python_version":"3.12","environment":{"kind":"pip"},"gpu":true}"#,
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"}}"#,
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"},"gpu":1}"#,
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"},"gpu":false}"#,
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"},"gpu":"true"}"#,
r#"{"kind":"python_v2","python_version":"3.12","environment":{"kind":"pip"},"gpu":"H100"}"#,
] {
assert!(serde_json::from_str::<PythonRuntimeSpec>(invalid).is_err());
}
}
#[test]
fn unknown_runtime_discards_payload_before_known_field_validation() {
for encoded in [
r#"{"kind":"python_v3","gpu":{"model":"H100"}}"#,
r#"{"kind":"python_v3","resources":[]}"#,
r#"{"kind":"python_v3","python_version":3.15,"environment":{"kind":[]}}"#,
] {
let runtime: PythonRuntimeSpec = serde_json::from_str(encoded).unwrap();
assert_eq!(runtime.kind(), "python_v3");
assert_eq!(
super::canonical_json(&runtime).unwrap(),
r#"{"kind":"python_v3"}"#
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_json_sorts_keys_at_every_depth() {
let value = serde_json::json!({
"runtime": {"kind": "python", "env": {"B": "2", "A": "1"}},
"artifact": {"digest": "sha256:x"},
"name": "embed",
});
let mut out = String::new();
write_canonical_json(&value, &mut out).expect("canonical JSON");
assert_eq!(
out,
r#"{"artifact":{"digest":"sha256:x"},"name":"embed","runtime":{"env":{"A":"1","B":"2"},"kind":"python"}}"#
);
}
#[test]
fn function_version_carries_its_namespace_as_a_separate_field() {
const VERSION: &str = include_str!(
"../tests/fixtures/first_class_functions/v1/remote_function_version.canonical.json"
);
let root = FunctionVersion::from_json(VERSION).unwrap();
assert!(root.namespace_path().is_empty());
assert_eq!(root.to_canonical_json().unwrap(), VERSION.trim_end());
let mut document: Value = serde_json::from_str(VERSION).unwrap();
document["namespace_path"] = serde_json::json!(["analytics", "features"]);
let nested = FunctionVersion::from_json(&document.to_string()).unwrap();
assert_eq!(nested.name(), "embed");
assert_eq!(nested.namespace_path(), ["analytics", "features"]);
let reference = nested.reference();
assert_eq!(reference.name, "embed");
assert_eq!(reference.namespace_path, ["analytics", "features"]);
let reference: Value = serde_json::to_value(&reference).unwrap();
assert_eq!(
reference["namespace_path"],
serde_json::json!(["analytics", "features"])
);
let root_reference = serde_json::to_value(root.reference()).unwrap();
assert!(root_reference.get("namespace_path").is_none());
let application = serde_json::json!({
"function": reference,
"inputs": [],
"output": serde_json::to_value(nested.signature().output.clone()).unwrap(),
});
let application = FunctionApplication::from_json(&application.to_string()).unwrap();
assert!(!application.has_unknown_fields());
assert_eq!(
application.function().namespace_path,
["analytics", "features"]
);
}
#[test]
fn canonical_json_preserves_array_order() {
let value = serde_json::json!({"inputs": ["b", "a", "c"]});
let mut out = String::new();
write_canonical_json(&value, &mut out).expect("canonical JSON");
assert_eq!(out, r#"{"inputs":["b","a","c"]}"#);
}
#[test]
fn validate_literal_rejects_floats_at_any_depth() {
for value in [
serde_json::json!(1.5),
serde_json::json!([1, [2, 3.5]]),
serde_json::json!({"a": {"b": 0.25}}),
] {
let error = validate_literal(&value).expect_err("floats are not canonical");
assert!(
error.to_string().contains("floating-point"),
"unexpected error: {error}"
);
}
for value in [
serde_json::json!(1),
serde_json::json!("1.5"),
serde_json::json!([1, {"a": true}]),
serde_json::json!(null),
] {
validate_literal(&value).expect("non-float literals are canonical");
}
}
#[test]
fn empty_initialization_is_omitted_from_the_wire() {
let signature: FunctionSignature = serde_json::from_str(
r#"{"inputs":[{"name":"x","arrow_type":"int64","nullable":true}],"output":{"kind":"scalar","arrow_type":"int64","nullable":false}}"#,
)
.unwrap();
assert!(signature.initialization.is_empty());
assert!(
!canonical_json(&signature)
.unwrap()
.contains("initialization")
);
let signature: FunctionSignature = serde_json::from_str(
r#"{"inputs":[],"output":{"kind":"scalar","arrow_type":"int64","nullable":false},"initialization":[{"name":"factor","arrow_type":"float64","nullable":false}]}"#,
)
.unwrap();
assert_eq!(signature.initialization[0].name, "factor");
assert!(
canonical_json(&signature)
.unwrap()
.contains(r#""initialization":[{"#)
);
}
#[test]
fn application_initialization_accepts_floats() {
let application = FunctionApplication::from_json(
r#"{"function":{"name":"scale","object_id":"o","location":"l","version":"1","manifest_digest":"sha256:x"},"inputs":[{"parameter":"x","kind":"column","value":{"path":"x"}}],"output":{"kind":"scalar","arrow_type":"float64","nullable":false},"initialization":{"factor":2.5,"label":"fast"}}"#,
)
.unwrap();
assert!(!application.has_unknown_fields());
assert_eq!(
application.initialization()["factor"],
serde_json::json!(2.5)
);
let encoded = application.to_canonical_json().unwrap();
assert!(encoded.contains(r#""initialization":{"factor":2.5,"label":"fast"}"#));
}
#[test]
fn binding_initialization_round_trips() {
let binding = FunctionBinding::from_json(
r#"{"binding_id":"fb_1","function":{"name":"scale","object_id":"o","location":"l","version":"1","manifest_digest":"sha256:x"},"inputs":[],"outputs":[],"initialization":"QUJD"}"#,
)
.unwrap();
assert_eq!(binding.initialization(), Some("QUJD"));
assert!(
binding
.to_canonical_json()
.unwrap()
.ends_with(r#""initialization":"QUJD","inputs":[],"outputs":[]}"#)
);
}
#[test]
fn has_unknown_keys_only_inspects_the_level_it_is_given() {
let value = serde_json::json!({"name": "embed", "version": "fv_1"});
assert!(!has_unknown_keys(&value, &["name", "version"]));
assert!(has_unknown_keys(&value, &["name"]));
let nested = serde_json::json!({"name": {"unexpected": 1}});
assert!(!has_unknown_keys(&nested, &["name"]));
assert!(!has_unknown_keys(&serde_json::json!("embed"), &["name"]));
}
}