614 lines
33 KiB
Python
614 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministically generate the small language-neutral Phase 3A bindings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
SCHEMA_PATH = ROOT / "schemas/control-v1.schema.json"
|
|
VERSION_PATH = ROOT / "VERSION"
|
|
COMPATIBILITY_PATH = ROOT / "compatibility.json"
|
|
|
|
|
|
def load_schema() -> tuple[dict[str, Any], str, str, dict[str, Any]]:
|
|
raw = SCHEMA_PATH.read_bytes()
|
|
schema = json.loads(raw)
|
|
version = VERSION_PATH.read_text(encoding="utf-8").strip()
|
|
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
|
|
raise ValueError("VERSION must be semantic version text")
|
|
compatibility = json.loads(COMPATIBILITY_PATH.read_text(encoding="utf-8"))
|
|
for key in ("current", "n_minus_1", "n_minus_2"):
|
|
if not isinstance(compatibility.get(key), str):
|
|
raise ValueError("compatibility declaration is incomplete")
|
|
return schema, hashlib.sha256(raw).hexdigest(), version, compatibility
|
|
|
|
|
|
def pascal(name: str) -> str:
|
|
return "".join(part[:1].upper() + part[1:] for part in re.split(r"[_-]", name))
|
|
|
|
|
|
def go_field(name: str) -> str:
|
|
initialisms = {"api": "API", "http": "HTTP", "id": "ID", "ip": "IP", "sha": "SHA", "ttl": "TTL", "url": "URL", "uuid": "UUID"}
|
|
return "".join(
|
|
initialisms.get(part, part[:1].upper() + part[1:])
|
|
for part in re.split(r"[_-]", name)
|
|
)
|
|
|
|
|
|
def camel(name: str) -> str:
|
|
parts = re.split(r"[_-]", name)
|
|
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
|
|
|
|
|
|
def swift_field(name: str) -> str:
|
|
value = camel(name)
|
|
if value in {"protocol", "class", "struct", "enum", "extension", "private", "public", "internal", "fileprivate", "open", "func", "let", "var", "import", "switch", "case", "default", "operator", "where", "repeat", "return", "throw", "throws", "try", "catch", "defer", "in", "is", "as"}:
|
|
return value + "Value"
|
|
return value
|
|
|
|
|
|
RUST_KEYWORDS = {
|
|
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
|
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
|
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
|
"super", "trait", "true", "type", "unsafe", "use", "where", "while", "async",
|
|
"await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override",
|
|
"priv", "typeof", "unsized", "virtual", "yield", "try", "union",
|
|
}
|
|
|
|
|
|
def rust_field(name: str) -> str:
|
|
value = camel(name)
|
|
return value + "Value" if value in RUST_KEYWORDS else value
|
|
|
|
|
|
def ref_name(value: Any) -> str | None:
|
|
if isinstance(value, dict) and isinstance(value.get("$ref"), str):
|
|
return value["$ref"].split("/")[-1]
|
|
return None
|
|
|
|
|
|
def prop_type(prop: dict[str, Any], language: str) -> str:
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
return reference if language == "go" else (reference if language == "rust" else reference)
|
|
if prop.get("type") == "array":
|
|
item = prop.get("items", {})
|
|
item_type = prop_type(item, language)
|
|
if language == "go":
|
|
return f"[]{item_type}"
|
|
if language == "rust":
|
|
return f"Vec<{item_type}>"
|
|
return f"[{item_type}]"
|
|
if prop.get("type") == "integer":
|
|
return "int64" if language == "go" else ("i64" if language == "rust" else "Int64")
|
|
if prop.get("type") == "boolean":
|
|
return "bool" if language != "swift" else "Bool"
|
|
if prop.get("type") == "object":
|
|
return "map[string]any" if language == "go" else ("JsonObject" if language == "rust" else "JSONObject")
|
|
return "string" if language == "go" else ("String" if language == "rust" else "String")
|
|
|
|
|
|
def go_zero(prop: dict[str, Any], field: str) -> str:
|
|
typ = prop_type(prop, "go")
|
|
if typ in {"string", "int64", "bool"}:
|
|
zero = {"string": '""', "int64": "0", "bool": "false"}[typ]
|
|
return f"v.{field} == {zero}"
|
|
if typ.startswith("[]"):
|
|
return f"v.{field} == nil"
|
|
if typ == "map[string]any":
|
|
return f"v.{field} == nil"
|
|
return f"reflect.DeepEqual(v.{field}, {typ}{{}})"
|
|
|
|
|
|
def go_validation(definition: dict[str, Any]) -> list[str]:
|
|
lines: list[str] = []
|
|
name = definition["name"]
|
|
required = set(definition.get("required", []))
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = go_field(prop_name)
|
|
required_zero_is_valid = prop.get("type") == "integer" and prop.get("minimum") == 0
|
|
if prop_name in required and prop.get("type") != "boolean" and not required_zero_is_valid:
|
|
zero = go_zero(prop, field)
|
|
lines.append(f"\tif {zero} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"required\"}}) }}")
|
|
if prop.get("type") == "string":
|
|
if "minLength" in prop:
|
|
lines.append(f"\tif len(v.{field}) < {prop['minLength']} && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_length\"}}) }}")
|
|
if "maxLength" in prop:
|
|
lines.append(f"\tif len(v.{field}) > {prop['maxLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_length\"}}) }}")
|
|
if "const" in prop:
|
|
lines.append(f"\tif v.{field} != \"{prop['const']}\" && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}")
|
|
if "enum" in prop:
|
|
allowed = " || ".join(f'v.{field} == "{value}"' for value in prop["enum"])
|
|
lines.append(f"\tif v.{field} != \"\" && !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}")
|
|
if prop.get("format") == "date-time":
|
|
lines.append(
|
|
'\tif v.%s != "" { if parsed, err := time.Parse(time.RFC3339Nano, v.%s); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.%s { violations = append(violations, FieldViolation{Field: "%s", Code: "invalid_time"}) } }'
|
|
% (field, field, field, prop_name)
|
|
)
|
|
if prop.get("type") == "integer":
|
|
if "minimum" in prop:
|
|
lines.append(f"\tif v.{field} != 0 && v.{field} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
|
|
if "maximum" in prop:
|
|
lines.append(f"\tif v.{field} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
|
|
if prop.get("type") == "array":
|
|
if "minItems" in prop:
|
|
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
|
|
if "maxItems" in prop:
|
|
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
|
|
item_ref = ref_name(prop.get("items", {}))
|
|
if item_ref:
|
|
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}")
|
|
if name in {"AllocationPolicy", "ManifestBounds"}:
|
|
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
|
|
if name == "GatewayRegistration":
|
|
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
|
|
if name == "ChannelFrame":
|
|
lines.append("\tif v.FragmentIndex >= v.FragmentCount { violations = append(violations, FieldViolation{Field: \"fragment_index\", Code: \"invalid_order\"}) }")
|
|
return lines
|
|
|
|
|
|
def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str, compatibility: dict[str, Any]) -> str:
|
|
out = [
|
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
|
"package protocol",
|
|
"",
|
|
"import (",
|
|
"\"bytes\"",
|
|
"\"encoding/json\"",
|
|
"\"errors\"",
|
|
"\"fmt\"",
|
|
"\"reflect\"",
|
|
"\"strings\"",
|
|
"\"time\"",
|
|
")",
|
|
"",
|
|
f'const SchemaSHA256 = "{schema_hash}"',
|
|
f'const ProtocolVersion = "{version}"',
|
|
f'const CurrentWireVersion = "{compatibility["current"]}"',
|
|
f'const NMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
|
f'const NMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
|
"",
|
|
"type FieldViolation struct {",
|
|
"\tField string `json:\"field\"`",
|
|
"\tCode string `json:\"code\"`",
|
|
"}",
|
|
"",
|
|
"type ValidationError struct {",
|
|
"\tViolations []FieldViolation",
|
|
"}",
|
|
"",
|
|
"func (e ValidationError) Error() string { return \"protocol validation failed\" }",
|
|
"",
|
|
]
|
|
for name in sorted(defs):
|
|
if name == "FieldViolation":
|
|
continue
|
|
definition = defs[name]
|
|
out.append(f"type {name} struct {{")
|
|
required = set(definition.get("required", []))
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
tag = prop_name + (",omitempty" if prop_name not in required else "")
|
|
out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`")
|
|
out.extend(["}", ""])
|
|
for name in sorted(defs):
|
|
out.append(f"func (v {name}) Validate() error {{")
|
|
out.append("\tvar violations []FieldViolation")
|
|
out.extend(go_validation(defs[name]))
|
|
out.append("\tif len(violations) > 0 { return ValidationError{Violations: violations} }")
|
|
out.append("\treturn nil")
|
|
out.append("}")
|
|
out.append("")
|
|
out.append(f"func Decode{name}(data []byte) ({name}, error) {{")
|
|
out.append(f"\tvar value {name}")
|
|
out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }")
|
|
out.append("\tvar fields map[string]json.RawMessage")
|
|
out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }")
|
|
required_fields = sorted(defs[name].get("required", []))
|
|
for prop_name in required_fields:
|
|
out.append(
|
|
'\tif raw, ok := fields["%s"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "required"}}} }'
|
|
% (prop_name, prop_name)
|
|
)
|
|
for prop_name, prop in defs[name].get("properties", {}).items():
|
|
if "x-max-bytes" in prop:
|
|
out.append(
|
|
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
|
|
% (prop_name, prop["x-max-bytes"], prop_name)
|
|
)
|
|
out.append("\tdecoder := json.NewDecoder(bytes.NewReader(data))")
|
|
out.append("\tdecoder.DisallowUnknownFields()")
|
|
out.append("\tif err := decoder.Decode(&value); err != nil { return value, err }")
|
|
out.append("\tvar trailing any")
|
|
out.append("\tif err := decoder.Decode(&trailing); err != io.EOF { if err == nil { return value, errors.New(\"trailing JSON value\") }; return value, err }")
|
|
out.append("\tif err := value.Validate(); err != nil { return value, err }")
|
|
out.append("\treturn value, nil")
|
|
out.append("}")
|
|
out.append("")
|
|
out.append(f"func Encode{name}(value {name}) ([]byte, error) {{")
|
|
out.append("\tif err := value.Validate(); err != nil { return nil, err }")
|
|
out.append("\treturn json.Marshal(value)")
|
|
out.append("}")
|
|
out.append("")
|
|
out.extend([
|
|
"var ErrNoCapabilityOverlap = errors.New(\"no capability overlap\")",
|
|
"",
|
|
"func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfile, error) {",
|
|
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
|
"\tselected := profiles[0]",
|
|
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
|
"\tfor _, profile := range profiles[1:] {",
|
|
"\t\tif err := profile.Validate(); err != nil || profile != selected { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
|
"\t}",
|
|
"\treturn selected, nil",
|
|
"}",
|
|
"",
|
|
])
|
|
out.extend([
|
|
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
|
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
|
|
"\tvar transcript strings.Builder",
|
|
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
|
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
|
"\treturn []byte(transcript.String())",
|
|
"}",
|
|
"",
|
|
])
|
|
# Use io.EOF in generated code without making every generated decoder depend on
|
|
# error-string comparison; replace the deliberately compact placeholder.
|
|
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
|
text = text.replace('!errors.Is(err, errors.New("EOF")) && err != nil', '!errors.Is(err, io.EOF)')
|
|
return text + "\n"
|
|
|
|
|
|
def rust_type(prop: dict[str, Any]) -> str:
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
return reference
|
|
if prop.get("type") == "array":
|
|
return f"Vec<{rust_type(prop.get('items', {}))}>"
|
|
if prop.get("type") == "integer":
|
|
return "i64"
|
|
if prop.get("type") == "boolean":
|
|
return "bool"
|
|
if prop.get("type") == "object":
|
|
return "JsonObject"
|
|
return "String"
|
|
|
|
|
|
def swift_type(prop: dict[str, Any]) -> str:
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
return reference
|
|
if prop.get("type") == "array":
|
|
return f"[{swift_type(prop.get('items', {}))}]"
|
|
if prop.get("type") == "integer":
|
|
return "Int64"
|
|
if prop.get("type") == "boolean":
|
|
return "Bool"
|
|
if prop.get("type") == "object":
|
|
return "JSONObject"
|
|
return "String"
|
|
|
|
|
|
def rust_validation(definition: dict[str, Any]) -> list[str]:
|
|
lines: list[str] = []
|
|
required = set(definition.get("required", []))
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = rust_field(prop_name)
|
|
value = f"self.{field}"
|
|
if prop_name not in required:
|
|
value = f"value"
|
|
lines.append(f" if let Some(value) = &self.{field} {{")
|
|
prefix, suffix = " ", " }"
|
|
else:
|
|
prefix, suffix = "", ""
|
|
if prop.get("type") == "string":
|
|
if prop_name in required and prop.get("minLength", 0) > 0:
|
|
lines.append(f" {prefix}if {value}.is_empty() {{ return Err(ValidationError::new(\"{prop_name}\", \"required\")); }}")
|
|
if "minLength" in prop:
|
|
lines.append(f" {prefix}if !{value}.is_empty() && {value}.len() < {prop['minLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_length\")); }}")
|
|
if "maxLength" in prop:
|
|
lines.append(f" {prefix}if {value}.len() > {prop['maxLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_length\")); }}")
|
|
if "const" in prop:
|
|
lines.append(f" {prefix}if {value} != \"{prop['const']}\" {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
|
|
if "enum" in prop:
|
|
allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"])
|
|
lines.append(f" {prefix}if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
|
|
if prop.get("type") == "integer":
|
|
if "minimum" in prop:
|
|
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
|
|
if "maximum" in prop:
|
|
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}")
|
|
if prop.get("type") == "array":
|
|
if "minItems" in prop:
|
|
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
|
|
if "maxItems" in prop:
|
|
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
|
|
item_ref = ref_name(prop.get("items", {}))
|
|
if item_ref:
|
|
lines.append(f" {prefix}for item in {value}.iter() {{ item.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_item\"))?; }}")
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
lines.append(f" {prefix}{value}.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_object\"))?;")
|
|
if suffix:
|
|
lines.append(suffix)
|
|
name = definition["name"]
|
|
if name in {"AllocationPolicy", "ManifestBounds"}:
|
|
lines.append(" if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new(\"bounds\", \"invalid_order\")); }")
|
|
if name == "GatewayRegistration":
|
|
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
|
|
if name == "ChannelFrame":
|
|
lines.append(" if self.fragmentIndex >= self.fragmentCount { return Err(ValidationError::new(\"fragment_index\", \"invalid_order\")); }")
|
|
return lines
|
|
|
|
|
|
def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
|
out = [
|
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
|
"#![allow(non_snake_case)]",
|
|
"pub const SCHEMA_SHA256: &str = \"" + schema_hash + "\";",
|
|
f'pub const CURRENT_WIRE_VERSION: &str = "{compatibility["current"]}";',
|
|
f'pub const N_MINUS_1_WIRE_VERSION: &str = "{compatibility["n_minus_1"]}";',
|
|
f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";',
|
|
"pub type JsonObject = std::collections::BTreeMap<String, String>;",
|
|
"",
|
|
"#[derive(Debug, Clone, PartialEq, Eq)]",
|
|
"pub struct ValidationError { pub field: &'static str, pub code: &'static str }",
|
|
"impl ValidationError { pub const fn new(field: &'static str, code: &'static str) -> Self { Self { field, code } } }",
|
|
"",
|
|
]
|
|
for name in sorted(defs):
|
|
definition = defs[name]
|
|
out.extend(["#[derive(Debug, Clone, PartialEq, Eq)]", f"pub struct {name} {{"])
|
|
required = set(definition.get("required", []))
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = rust_field(prop_name)
|
|
typ = rust_type(prop)
|
|
if prop_name not in required:
|
|
typ = f"Option<{typ}>"
|
|
out.append(f" {field}: {typ},")
|
|
out.extend(["}", ""])
|
|
parameters: list[str] = []
|
|
assignments: list[str] = []
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = rust_field(prop_name)
|
|
typ = rust_type(prop)
|
|
if prop_name not in required:
|
|
typ = f"Option<{typ}>"
|
|
parameters.append(f"{field}: {typ}")
|
|
assignments.append(field)
|
|
out.append(f"impl {name} {{")
|
|
out.append(f" pub fn new({', '.join(parameters)}) -> Result<Self, ValidationError> {{")
|
|
out.append(f" let value = Self {{ {', '.join(assignments)} }};")
|
|
out.append(" value.validate()?;")
|
|
out.append(" Ok(value)")
|
|
out.append(" }")
|
|
out.append(" pub fn validate(&self) -> Result<(), ValidationError> {")
|
|
out.extend(rust_validation(definition))
|
|
out.append(" Ok(())")
|
|
out.append(" }")
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = rust_field(prop_name)
|
|
typ = rust_type(prop)
|
|
if prop_name not in required:
|
|
typ = f"Option<{typ}>"
|
|
out.append(f" pub fn {field}(&self) -> &{typ} {{ &self.{field} }}")
|
|
if name == "TunnelAdmissionRequest":
|
|
out.extend([
|
|
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
|
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
|
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
|
|
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
|
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
|
" transcript.into_bytes()",
|
|
" }",
|
|
])
|
|
out.extend(["}", ""])
|
|
out.extend([
|
|
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
|
" let selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
|
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
|
" for profile in &profiles[1..] {",
|
|
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
|
" if profile != &selected { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
|
" }",
|
|
" Ok(selected)",
|
|
"}",
|
|
"",
|
|
])
|
|
return "\n".join(out)
|
|
|
|
|
|
def swift_validation(definition: dict[str, Any]) -> list[str]:
|
|
lines: list[str] = []
|
|
required = set(definition.get("required", []))
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = swift_field(prop_name)
|
|
value = f"self.{field}"
|
|
if prop_name not in required:
|
|
value = "value"
|
|
lines.append(f" if let value = self.{field} {{")
|
|
prefix, suffix = " ", " }"
|
|
else:
|
|
prefix, suffix = "", ""
|
|
if prop.get("type") == "string":
|
|
if prop_name in required and prop.get("minLength", 0) > 0:
|
|
lines.append(f" {prefix}if {value}.isEmpty {{ throw ContractValidationError(field: \"{prop_name}\", code: \"required\") }}")
|
|
if "minLength" in prop:
|
|
lines.append(f" {prefix}if !{value}.isEmpty && {value}.utf8.count < {prop['minLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_length\") }}")
|
|
if "maxLength" in prop:
|
|
lines.append(f" {prefix}if {value}.utf8.count > {prop['maxLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_length\") }}")
|
|
if "const" in prop:
|
|
lines.append(f" {prefix}if {value} != \"{prop['const']}\" {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
|
|
if "enum" in prop:
|
|
allowed = ", ".join(f'\"{item}\"' for item in prop["enum"])
|
|
lines.append(f" {prefix}if ![{allowed}].contains({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
|
|
if prop.get("format") == "date-time":
|
|
lines.append(f" {prefix}if ISO8601DateFormatter().date(from: {value}) == nil {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
|
if prop.get("type") == "integer":
|
|
if "minimum" in prop:
|
|
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
|
|
if "maximum" in prop:
|
|
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"maximum\") }}")
|
|
if prop.get("type") == "array":
|
|
if "minItems" in prop:
|
|
lines.append(f" {prefix}if {value}.count < {prop['minItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_items\") }}")
|
|
if "maxItems" in prop:
|
|
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
|
|
item_ref = ref_name(prop.get("items", {}))
|
|
if item_ref:
|
|
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
|
reference = ref_name(prop)
|
|
if reference:
|
|
lines.append(f" {prefix}try {value}.validate()")
|
|
if suffix:
|
|
lines.append(suffix)
|
|
name = definition["name"]
|
|
if name in {"AllocationPolicy", "ManifestBounds"}:
|
|
lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }")
|
|
if name == "GatewayRegistration":
|
|
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
|
|
if name == "ChannelFrame":
|
|
lines.append(" if fragmentIndex >= fragmentCount { throw ContractValidationError(field: \"fragment_index\", code: \"invalid_order\") }")
|
|
return lines
|
|
|
|
|
|
def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
|
out = [
|
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
|
"import Foundation",
|
|
"public typealias JSONObject = [String: String]",
|
|
f"public let schemaSHA256 = \"{schema_hash}\"",
|
|
f'public let currentWireVersion = "{compatibility["current"]}"',
|
|
f'public let nMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
|
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
|
"public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }",
|
|
"private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }",
|
|
"",
|
|
]
|
|
for name in sorted(defs):
|
|
definition = defs[name]
|
|
required = set(definition.get("required", []))
|
|
out.extend(["public struct " + name + ": Codable, Equatable {",])
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
typ = swift_type(prop)
|
|
if prop_name not in required:
|
|
typ += "?"
|
|
out.append(f" public let {swift_field(prop_name)}: {typ}")
|
|
out.append(" enum CodingKeys: String, CodingKey {")
|
|
for prop_name in definition.get("properties", {}):
|
|
out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"")
|
|
parameters: list[str] = []
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
typ = swift_type(prop)
|
|
if prop_name not in required:
|
|
typ += "?"
|
|
parameters.append(f"{swift_field(prop_name)}: {typ}")
|
|
out.extend([" }", "", f" public init({', '.join(parameters)}) throws {{"])
|
|
for prop_name in definition.get("properties", {}):
|
|
field = swift_field(prop_name)
|
|
out.append(f" self.{field} = {field}")
|
|
out.extend([" try validate()", " }", "", " public init(from decoder: Decoder) throws {"])
|
|
out.append(" let all = try decoder.container(keyedBy: AnyCodingKey.self)")
|
|
out.append(" for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: \"unknown_field\") }")
|
|
out.append(" let c = try decoder.container(keyedBy: CodingKeys.self)")
|
|
decoded: list[str] = []
|
|
for prop_name, prop in definition.get("properties", {}).items():
|
|
field = swift_field(prop_name)
|
|
typ = swift_type(prop)
|
|
if prop_name in required:
|
|
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})")
|
|
else:
|
|
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
|
|
out.append(f" try self.init({', '.join(decoded)})")
|
|
out.extend([" }", "", " public func validate() throws {"])
|
|
out.extend(swift_validation(definition))
|
|
out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
|
|
out.extend([
|
|
"public extension TunnelAdmissionRequest {",
|
|
" func deviceAdmissionTranscript() -> Data {",
|
|
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
|
|
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
|
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
|
" return Data(transcript.utf8)",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"public extension CapabilityProfile {",
|
|
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
|
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
|
" try selected.validate()",
|
|
" for profile in profiles.dropFirst() {",
|
|
" try profile.validate()",
|
|
" if profile != selected { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
|
" }",
|
|
" return selected",
|
|
" }",
|
|
"}",
|
|
"",
|
|
])
|
|
return "\n".join(out)
|
|
|
|
|
|
def write_or_check(path: pathlib.Path, content: str, check: bool) -> None:
|
|
if check:
|
|
if not path.exists() or path.read_text(encoding="utf-8") != content:
|
|
raise ValueError(f"generated output differs: {path.relative_to(ROOT)}")
|
|
return
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def format_go(content: str) -> str:
|
|
result = subprocess.run(["gofmt"], input=content, text=True, capture_output=True, check=False)
|
|
if result.returncode != 0:
|
|
raise ValueError(f"gofmt failed: {result.stderr.strip()}")
|
|
return result.stdout
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--check", action="store_true")
|
|
args = parser.parse_args()
|
|
schema, schema_hash, version, compatibility = load_schema()
|
|
generator_hash = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest()
|
|
defs = schema.get("$defs")
|
|
if not isinstance(defs, dict) or not defs:
|
|
raise ValueError("schema must contain non-empty $defs")
|
|
normalized = {name: dict(value, name=name) for name, value in defs.items()}
|
|
outputs = {
|
|
ROOT / "gen/go/protocol/protocol.go": format_go(generate_go(normalized, schema_hash, version, compatibility)),
|
|
ROOT / "gen/rust/protocol.rs": generate_rust(normalized, schema_hash, compatibility).rstrip() + "\n",
|
|
ROOT / "gen/swift/Protocol.swift": generate_swift(normalized, schema_hash, compatibility).rstrip() + "\n",
|
|
ROOT / "gen/manifest.json": json.dumps({
|
|
"generator_sha256": generator_hash,
|
|
"schema_sha256": schema_hash,
|
|
"protocol_version": version,
|
|
"compatibility": compatibility,
|
|
}, sort_keys=True, indent=2) + "\n",
|
|
}
|
|
for path, content in outputs.items():
|
|
write_or_check(path, content, args.check)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
print(f"generate: {exc}", file=sys.stderr)
|
|
raise SystemExit(1)
|