use std::collections::BTreeMap;
use serde::de::{self, DeserializeOwned};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
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())))
}
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, &["name", "version"]))
{
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,
}
#[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 FunctionVersion {
name: String,
version: String,
artifact: FunctionArtifact,
signature: FunctionSignature,
runtime: PythonRuntimeSpec,
runtime_digest: String,
environment_digest: String,
created_at: String,
}
impl FunctionVersion {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn artifact(&self) -> &FunctionArtifact {
&self.artifact
}
pub fn signature(&self) -> &FunctionSignature {
&self.signature
}
pub fn runtime(&self) -> &PythonRuntimeSpec {
&self.runtime
}
pub fn runtime_digest(&self) -> &str {
&self.runtime_digest
}
pub fn environment_digest(&self) -> &str {
&self.environment_digest
}
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,
}
impl_json!(FunctionRegistrationRequest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionVersionRef {
pub name: String,
pub version: 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, 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 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 FunctionBinding {
binding_id: String,
function: FunctionVersionRef,
inputs: Vec<InputBinding>,
outputs: Vec<OutputMapping>,
#[serde(default, skip_serializing_if = "Option::is_none")]
input_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
output_schema: Option<Value>,
}
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 input_schema(&self) -> Option<&Value> {
self.input_schema.as_ref()
}
pub fn output_schema(&self) -> Option<&Value> {
self.output_schema.as_ref()
}
}
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>,
}
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"}"#
);
}
}
}