diff --git a/Makefile b/Makefile index d70bbd1..332d0f4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile clean-generated +.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated PYTHON ?= python3 PROTOC ?= protoc @@ -29,6 +29,9 @@ binding-compile: rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib swiftc -typecheck gen/swift/Protocol.swift +strict-contracts: + $(PYTHON) -B tools/test_generated_contracts.py + conformance: $(PYTHON) -B tools/fixture_digest.py go run ./tools/go-conformance @@ -40,4 +43,4 @@ frame-verify: clean-generated: $(PYTHON) tools/generate.py --check -verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile conformance frame-verify clean-generated +verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated diff --git a/gen/go/protocol/protocol.go b/gen/go/protocol/protocol.go index bf7ad87..68b8ecf 100644 --- a/gen/go/protocol/protocol.go +++ b/gen/go/protocol/protocol.go @@ -414,6 +414,9 @@ func (v AllocationPolicy) Validate() error { if v.ReservationLeaseSeconds > 3600 { violations = append(violations, FieldViolation{Field: "reservation_lease_seconds", Code: "maximum"}) } + if v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { + violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_order"}) + } if len(violations) > 0 { return ValidationError{Violations: violations} } @@ -918,6 +921,9 @@ func (v ChannelFrame) Validate() error { if len(v.Payload) > 87384 { violations = append(violations, FieldViolation{Field: "payload", Code: "max_length"}) } + if v.FragmentIndex >= v.FragmentCount { + violations = append(violations, FieldViolation{Field: "fragment_index", Code: "invalid_order"}) + } if len(violations) > 0 { return ValidationError{Violations: violations} } @@ -2227,6 +2233,9 @@ func (v GatewayRegistration) Validate() error { if err := v.Capabilities.Validate(); err != nil { violations = append(violations, FieldViolation{Field: "capabilities", Code: "invalid_object"}) } + if v.ProtocolMinVersion > v.ProtocolMaxVersion { + violations = append(violations, FieldViolation{Field: "protocol_version", Code: "invalid_order"}) + } if len(violations) > 0 { return ValidationError{Violations: violations} } @@ -2484,6 +2493,9 @@ func (v ManifestBounds) Validate() error { if v.MaximumKbps > 100000000 { violations = append(violations, FieldViolation{Field: "maximum_kbps", Code: "maximum"}) } + if v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { + violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_order"}) + } if len(violations) > 0 { return ValidationError{Violations: violations} } @@ -4043,3 +4055,21 @@ func EncodeVersionNegotiation(value VersionNegotiation) ([]byte, error) { } return json.Marshal(value) } + +var ErrNoCapabilityOverlap = errors.New("no capability overlap") + +func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfile, error) { + if len(profiles) == 0 { + return CapabilityProfile{}, ErrNoCapabilityOverlap + } + selected := profiles[0] + if err := selected.Validate(); err != nil { + return CapabilityProfile{}, ErrNoCapabilityOverlap + } + for _, profile := range profiles[1:] { + if err := profile.Validate(); err != nil || profile != selected { + return CapabilityProfile{}, ErrNoCapabilityOverlap + } + } + return selected, nil +} diff --git a/gen/manifest.json b/gen/manifest.json index 426e619..94b1dfd 100644 --- a/gen/manifest.json +++ b/gen/manifest.json @@ -12,7 +12,7 @@ "2" ] }, - "generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c", + "generator_sha256": "88535ecf2b1c926104b10b4ab56f1bc6298e1c3e75490e36ee8581a135bcded7", "protocol_version": "1.0.0", "schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec" } diff --git a/gen/rust/protocol.rs b/gen/rust/protocol.rs index 828ab36..bf2e324 100644 --- a/gen/rust/protocol.rs +++ b/gen/rust/protocol.rs @@ -6,352 +6,1413 @@ pub const N_MINUS_1_WIRE_VERSION: &str = "0"; pub const N_MINUS_2_WIRE_VERSION: &str = "-1"; pub type JsonObject = std::collections::BTreeMap; +#[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 } } } + #[derive(Debug, Clone, PartialEq, Eq)] pub struct AllocationPolicy { - pub minimumKbps: i64, - pub targetKbps: i64, - pub maximumKbps: i64, - pub tier: String, - pub audience: String, - pub protocol: String, - pub protocolVersion: i64, - pub grantTtlSeconds: i64, - pub reservationLeaseSeconds: i64, + minimumKbps: i64, + targetKbps: i64, + maximumKbps: i64, + tier: String, + audience: String, + protocol: String, + protocolVersion: i64, + grantTtlSeconds: i64, + reservationLeaseSeconds: i64, +} + +impl AllocationPolicy { + pub fn new(minimumKbps: i64, targetKbps: i64, maximumKbps: i64, tier: String, audience: String, protocol: String, protocolVersion: i64, grantTtlSeconds: i64, reservationLeaseSeconds: i64) -> Result { + let value = Self { minimumKbps, targetKbps, maximumKbps, tier, audience, protocol, protocolVersion, grantTtlSeconds, reservationLeaseSeconds }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.minimumKbps < 1 { return Err(ValidationError::new("minimum_kbps", "minimum")); } + if self.minimumKbps > 100000000 { return Err(ValidationError::new("minimum_kbps", "maximum")); } + if self.targetKbps < 1 { return Err(ValidationError::new("target_kbps", "minimum")); } + if self.targetKbps > 100000000 { return Err(ValidationError::new("target_kbps", "maximum")); } + if self.maximumKbps < 1 { return Err(ValidationError::new("maximum_kbps", "minimum")); } + if self.maximumKbps > 100000000 { return Err(ValidationError::new("maximum_kbps", "maximum")); } + if self.tier != "standard" && self.tier != "priority" && self.tier != "premium" { return Err(ValidationError::new("tier", "invalid_value")); } + if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); } + if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); } + if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); } + if self.protocol.is_empty() { return Err(ValidationError::new("protocol", "required")); } + if !self.protocol.is_empty() && self.protocol.len() < 1 { return Err(ValidationError::new("protocol", "min_length")); } + if self.protocol.len() > 64 { return Err(ValidationError::new("protocol", "max_length")); } + if self.protocolVersion < 1 { return Err(ValidationError::new("protocol_version", "minimum")); } + if self.protocolVersion > 100 { return Err(ValidationError::new("protocol_version", "maximum")); } + if self.grantTtlSeconds < 5 { return Err(ValidationError::new("grant_ttl_seconds", "minimum")); } + if self.grantTtlSeconds > 300 { return Err(ValidationError::new("grant_ttl_seconds", "maximum")); } + if self.reservationLeaseSeconds < 5 { return Err(ValidationError::new("reservation_lease_seconds", "minimum")); } + if self.reservationLeaseSeconds > 3600 { return Err(ValidationError::new("reservation_lease_seconds", "maximum")); } + if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new("bounds", "invalid_order")); } + Ok(()) + } + pub fn minimumKbps(&self) -> &i64 { &self.minimumKbps } + pub fn targetKbps(&self) -> &i64 { &self.targetKbps } + pub fn maximumKbps(&self) -> &i64 { &self.maximumKbps } + pub fn tier(&self) -> &String { &self.tier } + pub fn audience(&self) -> &String { &self.audience } + pub fn protocol(&self) -> &String { &self.protocol } + pub fn protocolVersion(&self) -> &i64 { &self.protocolVersion } + pub fn grantTtlSeconds(&self) -> &i64 { &self.grantTtlSeconds } + pub fn reservationLeaseSeconds(&self) -> &i64 { &self.reservationLeaseSeconds } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct AssignedDesktop { - pub assignmentId: String, - pub poolId: String, - pub name: String, - pub availability: String, + assignmentId: String, + poolId: String, + name: String, + availability: String, +} + +impl AssignedDesktop { + pub fn new(assignmentId: String, poolId: String, name: String, availability: String) -> Result { + let value = Self { assignmentId, poolId, name, availability }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.assignmentId.is_empty() { return Err(ValidationError::new("assignment_id", "required")); } + if !self.assignmentId.is_empty() && self.assignmentId.len() < 1 { return Err(ValidationError::new("assignment_id", "min_length")); } + if self.assignmentId.len() > 128 { return Err(ValidationError::new("assignment_id", "max_length")); } + if self.poolId.is_empty() { return Err(ValidationError::new("pool_id", "required")); } + if !self.poolId.is_empty() && self.poolId.len() < 1 { return Err(ValidationError::new("pool_id", "min_length")); } + if self.poolId.len() > 128 { return Err(ValidationError::new("pool_id", "max_length")); } + if self.name.is_empty() { return Err(ValidationError::new("name", "required")); } + if !self.name.is_empty() && self.name.len() < 1 { return Err(ValidationError::new("name", "min_length")); } + if self.name.len() > 256 { return Err(ValidationError::new("name", "max_length")); } + if self.availability.is_empty() { return Err(ValidationError::new("availability", "required")); } + if !self.availability.is_empty() && self.availability.len() < 1 { return Err(ValidationError::new("availability", "min_length")); } + if self.availability.len() > 64 { return Err(ValidationError::new("availability", "max_length")); } + Ok(()) + } + pub fn assignmentId(&self) -> &String { &self.assignmentId } + pub fn poolId(&self) -> &String { &self.poolId } + pub fn name(&self) -> &String { &self.name } + pub fn availability(&self) -> &String { &self.availability } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct BrokerSession { - pub id: String, - pub principalId: String, - pub poolId: String, - pub assignmentId: Option, - pub state: String, - pub policySnapshot: AllocationPolicy, - pub reconnectDeadline: Option, - pub outcome: Option, - pub failureCode: Option, - pub cleanupState: String, - pub idempotencyKey: String, - pub correlationId: String, - pub requestedAt: String, - pub endedAt: Option, - pub version: i64, + id: String, + principalId: String, + poolId: String, + assignmentId: Option, + state: String, + policySnapshot: AllocationPolicy, + reconnectDeadline: Option, + outcome: Option, + failureCode: Option, + cleanupState: String, + idempotencyKey: String, + correlationId: String, + requestedAt: String, + endedAt: Option, + version: i64, +} + +impl BrokerSession { + pub fn new(id: String, principalId: String, poolId: String, assignmentId: Option, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: Option, outcome: Option, failureCode: Option, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: Option, version: i64) -> Result { + let value = Self { id, principalId, poolId, assignmentId, state, policySnapshot, reconnectDeadline, outcome, failureCode, cleanupState, idempotencyKey, correlationId, requestedAt, endedAt, version }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.id.is_empty() { return Err(ValidationError::new("id", "required")); } + if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } + if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } + if self.principalId.is_empty() { return Err(ValidationError::new("principal_id", "required")); } + if !self.principalId.is_empty() && self.principalId.len() < 1 { return Err(ValidationError::new("principal_id", "min_length")); } + if self.principalId.len() > 128 { return Err(ValidationError::new("principal_id", "max_length")); } + if self.poolId.is_empty() { return Err(ValidationError::new("pool_id", "required")); } + if !self.poolId.is_empty() && self.poolId.len() < 1 { return Err(ValidationError::new("pool_id", "min_length")); } + if self.poolId.len() > 128 { return Err(ValidationError::new("pool_id", "max_length")); } + if let Some(value) = &self.assignmentId { + if value.len() > 128 { return Err(ValidationError::new("assignment_id", "max_length")); } + } + if self.state.is_empty() { return Err(ValidationError::new("state", "required")); } + if !self.state.is_empty() && self.state.len() < 1 { return Err(ValidationError::new("state", "min_length")); } + if self.state.len() > 64 { return Err(ValidationError::new("state", "max_length")); } + self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?; + if let Some(value) = &self.reconnectDeadline { + if value.len() > 64 { return Err(ValidationError::new("reconnect_deadline", "max_length")); } + } + if let Some(value) = &self.outcome { + if value.len() > 64 { return Err(ValidationError::new("outcome", "max_length")); } + } + if let Some(value) = &self.failureCode { + if value.len() > 128 { return Err(ValidationError::new("failure_code", "max_length")); } + } + if self.cleanupState.is_empty() { return Err(ValidationError::new("cleanup_state", "required")); } + if !self.cleanupState.is_empty() && self.cleanupState.len() < 1 { return Err(ValidationError::new("cleanup_state", "min_length")); } + if self.cleanupState.len() > 64 { return Err(ValidationError::new("cleanup_state", "max_length")); } + if self.idempotencyKey.is_empty() { return Err(ValidationError::new("idempotency_key", "required")); } + if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); } + if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); } + if self.correlationId.is_empty() { return Err(ValidationError::new("correlation_id", "required")); } + if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); } + if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); } + if self.requestedAt.len() > 64 { return Err(ValidationError::new("requested_at", "max_length")); } + if let Some(value) = &self.endedAt { + if value.len() > 64 { return Err(ValidationError::new("ended_at", "max_length")); } + } + if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } + Ok(()) + } + pub fn id(&self) -> &String { &self.id } + pub fn principalId(&self) -> &String { &self.principalId } + pub fn poolId(&self) -> &String { &self.poolId } + pub fn assignmentId(&self) -> &Option { &self.assignmentId } + pub fn state(&self) -> &String { &self.state } + pub fn policySnapshot(&self) -> &AllocationPolicy { &self.policySnapshot } + pub fn reconnectDeadline(&self) -> &Option { &self.reconnectDeadline } + pub fn outcome(&self) -> &Option { &self.outcome } + pub fn failureCode(&self) -> &Option { &self.failureCode } + pub fn cleanupState(&self) -> &String { &self.cleanupState } + pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey } + pub fn correlationId(&self) -> &String { &self.correlationId } + pub fn requestedAt(&self) -> &String { &self.requestedAt } + pub fn endedAt(&self) -> &Option { &self.endedAt } + pub fn version(&self) -> &i64 { &self.version } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CapabilityProfile { - pub transport: String, - pub framing: String, - pub media: String, - pub audio: String, - pub sourceRateControl: String, - pub clientDecode: String, + transport: String, + framing: String, + media: String, + audio: String, + sourceRateControl: String, + clientDecode: String, +} + +impl CapabilityProfile { + pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) -> Result { + let value = Self { transport, framing, media, audio, sourceRateControl, clientDecode }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); } + if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); } + if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); } + if self.framing.is_empty() { return Err(ValidationError::new("framing", "required")); } + if !self.framing.is_empty() && self.framing.len() < 1 { return Err(ValidationError::new("framing", "min_length")); } + if self.framing.len() > 64 { return Err(ValidationError::new("framing", "max_length")); } + if self.media.is_empty() { return Err(ValidationError::new("media", "required")); } + if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); } + if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); } + if self.audio.is_empty() { return Err(ValidationError::new("audio", "required")); } + if !self.audio.is_empty() && self.audio.len() < 1 { return Err(ValidationError::new("audio", "min_length")); } + if self.audio.len() > 64 { return Err(ValidationError::new("audio", "max_length")); } + if self.sourceRateControl.is_empty() { return Err(ValidationError::new("source_rate_control", "required")); } + if !self.sourceRateControl.is_empty() && self.sourceRateControl.len() < 1 { return Err(ValidationError::new("source_rate_control", "min_length")); } + if self.sourceRateControl.len() > 64 { return Err(ValidationError::new("source_rate_control", "max_length")); } + if self.clientDecode.is_empty() { return Err(ValidationError::new("client_decode", "required")); } + if !self.clientDecode.is_empty() && self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_length")); } + if self.clientDecode.len() > 64 { return Err(ValidationError::new("client_decode", "max_length")); } + Ok(()) + } + pub fn transport(&self) -> &String { &self.transport } + pub fn framing(&self) -> &String { &self.framing } + pub fn media(&self) -> &String { &self.media } + pub fn audio(&self) -> &String { &self.audio } + pub fn sourceRateControl(&self) -> &String { &self.sourceRateControl } + pub fn clientDecode(&self) -> &String { &self.clientDecode } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ChannelFrame { - pub version: String, - pub flowId: String, - pub sequence: i64, - pub flags: i64, - pub fragmentIndex: i64, - pub fragmentCount: i64, - pub timestampMs: i64, - pub payload: String, + version: String, + flowId: String, + sequence: i64, + flags: i64, + fragmentIndex: i64, + fragmentCount: i64, + timestampMs: i64, + payload: String, +} + +impl ChannelFrame { + pub fn new(version: String, flowId: String, sequence: i64, flags: i64, fragmentIndex: i64, fragmentCount: i64, timestampMs: i64, payload: String) -> Result { + let value = Self { version, flowId, sequence, flags, fragmentIndex, fragmentCount, timestampMs, payload }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.flowId.is_empty() { return Err(ValidationError::new("flow_id", "required")); } + if !self.flowId.is_empty() && self.flowId.len() < 1 { return Err(ValidationError::new("flow_id", "min_length")); } + if self.flowId.len() > 64 { return Err(ValidationError::new("flow_id", "max_length")); } + if self.sequence < 0 { return Err(ValidationError::new("sequence", "minimum")); } + if self.flags < 0 { return Err(ValidationError::new("flags", "minimum")); } + if self.flags > 255 { return Err(ValidationError::new("flags", "maximum")); } + if self.fragmentIndex < 0 { return Err(ValidationError::new("fragment_index", "minimum")); } + if self.fragmentIndex > 15 { return Err(ValidationError::new("fragment_index", "maximum")); } + if self.fragmentCount < 1 { return Err(ValidationError::new("fragment_count", "minimum")); } + if self.fragmentCount > 16 { return Err(ValidationError::new("fragment_count", "maximum")); } + if self.timestampMs < 0 { return Err(ValidationError::new("timestamp_ms", "minimum")); } + if self.payload.len() > 87384 { return Err(ValidationError::new("payload", "max_length")); } + if self.fragmentIndex >= self.fragmentCount { return Err(ValidationError::new("fragment_index", "invalid_order")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn flowId(&self) -> &String { &self.flowId } + pub fn sequence(&self) -> &i64 { &self.sequence } + pub fn flags(&self) -> &i64 { &self.flags } + pub fn fragmentIndex(&self) -> &i64 { &self.fragmentIndex } + pub fn fragmentCount(&self) -> &i64 { &self.fragmentCount } + pub fn timestampMs(&self) -> &i64 { &self.timestampMs } + pub fn payload(&self) -> &String { &self.payload } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClipboardText { - pub text: String, - pub encoding: String, + text: String, + encoding: String, +} + +impl ClipboardText { + pub fn new(text: String, encoding: String) -> Result { + let value = Self { text, encoding }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.text.len() > 65536 { return Err(ValidationError::new("text", "max_length")); } + if self.encoding != "utf-8" { return Err(ValidationError::new("encoding", "invalid_value")); } + Ok(()) + } + pub fn text(&self) -> &String { &self.text } + pub fn encoding(&self) -> &String { &self.encoding } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConnectionManifest { - pub version: String, - pub purpose: String, - pub sessionId: String, - pub reconnectSequence: i64, - pub gateway: ManifestGateway, - pub tunnel: ManifestTunnel, - pub profile: ManifestProfile, - pub grant: GrantReference, - pub correlationId: String, + version: String, + purpose: String, + sessionId: String, + reconnectSequence: i64, + gateway: ManifestGateway, + tunnel: ManifestTunnel, + profile: ManifestProfile, + grant: GrantReference, + correlationId: String, +} + +impl ConnectionManifest { + pub fn new(version: String, purpose: String, sessionId: String, reconnectSequence: i64, gateway: ManifestGateway, tunnel: ManifestTunnel, profile: ManifestProfile, grant: GrantReference, correlationId: String) -> Result { + let value = Self { version, purpose, sessionId, reconnectSequence, gateway, tunnel, profile, grant, correlationId }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.purpose != "launch" && self.purpose != "reconnect" { return Err(ValidationError::new("purpose", "invalid_value")); } + if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); } + if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); } + if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); } + if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } + self.gateway.validate().map_err(|_| ValidationError::new("gateway", "invalid_object"))?; + self.tunnel.validate().map_err(|_| ValidationError::new("tunnel", "invalid_object"))?; + self.profile.validate().map_err(|_| ValidationError::new("profile", "invalid_object"))?; + self.grant.validate().map_err(|_| ValidationError::new("grant", "invalid_object"))?; + if self.correlationId.is_empty() { return Err(ValidationError::new("correlation_id", "required")); } + if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); } + if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn purpose(&self) -> &String { &self.purpose } + pub fn sessionId(&self) -> &String { &self.sessionId } + pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence } + pub fn gateway(&self) -> &ManifestGateway { &self.gateway } + pub fn tunnel(&self) -> &ManifestTunnel { &self.tunnel } + pub fn profile(&self) -> &ManifestProfile { &self.profile } + pub fn grant(&self) -> &GrantReference { &self.grant } + pub fn correlationId(&self) -> &String { &self.correlationId } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeviceChallenge { - pub deviceId: String, - pub serverId: String, - pub principalId: String, - pub challenge: String, - pub expiresAt: String, - pub algorithm: String, - pub signatureFormat: String, + deviceId: String, + serverId: String, + principalId: String, + challenge: String, + expiresAt: String, + algorithm: String, + signatureFormat: String, +} + +impl DeviceChallenge { + pub fn new(deviceId: String, serverId: String, principalId: String, challenge: String, expiresAt: String, algorithm: String, signatureFormat: String) -> Result { + let value = Self { deviceId, serverId, principalId, challenge, expiresAt, algorithm, signatureFormat }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.deviceId.is_empty() { return Err(ValidationError::new("device_id", "required")); } + if !self.deviceId.is_empty() && self.deviceId.len() < 1 { return Err(ValidationError::new("device_id", "min_length")); } + if self.deviceId.len() > 128 { return Err(ValidationError::new("device_id", "max_length")); } + if self.serverId.is_empty() { return Err(ValidationError::new("server_id", "required")); } + if !self.serverId.is_empty() && self.serverId.len() < 1 { return Err(ValidationError::new("server_id", "min_length")); } + if self.serverId.len() > 128 { return Err(ValidationError::new("server_id", "max_length")); } + if self.principalId.is_empty() { return Err(ValidationError::new("principal_id", "required")); } + if !self.principalId.is_empty() && self.principalId.len() < 1 { return Err(ValidationError::new("principal_id", "min_length")); } + if self.principalId.len() > 128 { return Err(ValidationError::new("principal_id", "max_length")); } + if self.challenge.is_empty() { return Err(ValidationError::new("challenge", "required")); } + if !self.challenge.is_empty() && self.challenge.len() < 1 { return Err(ValidationError::new("challenge", "min_length")); } + if self.challenge.len() > 256 { return Err(ValidationError::new("challenge", "max_length")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + if self.algorithm != "ed25519" { return Err(ValidationError::new("algorithm", "invalid_value")); } + if self.signatureFormat != "ed25519-domain-separated-v1" { return Err(ValidationError::new("signature_format", "invalid_value")); } + Ok(()) + } + pub fn deviceId(&self) -> &String { &self.deviceId } + pub fn serverId(&self) -> &String { &self.serverId } + pub fn principalId(&self) -> &String { &self.principalId } + pub fn challenge(&self) -> &String { &self.challenge } + pub fn expiresAt(&self) -> &String { &self.expiresAt } + pub fn algorithm(&self) -> &String { &self.algorithm } + pub fn signatureFormat(&self) -> &String { &self.signatureFormat } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeviceProofRequest { - pub challenge: String, - pub signature: String, + challenge: String, + signature: String, +} + +impl DeviceProofRequest { + pub fn new(challenge: String, signature: String) -> Result { + let value = Self { challenge, signature }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.challenge.is_empty() { return Err(ValidationError::new("challenge", "required")); } + if !self.challenge.is_empty() && self.challenge.len() < 1 { return Err(ValidationError::new("challenge", "min_length")); } + if self.challenge.len() > 256 { return Err(ValidationError::new("challenge", "max_length")); } + if self.signature.is_empty() { return Err(ValidationError::new("signature", "required")); } + if !self.signature.is_empty() && self.signature.len() < 1 { return Err(ValidationError::new("signature", "min_length")); } + if self.signature.len() > 256 { return Err(ValidationError::new("signature", "max_length")); } + Ok(()) + } + pub fn challenge(&self) -> &String { &self.challenge } + pub fn signature(&self) -> &String { &self.signature } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeviceRegistrationRequest { - pub name: String, - pub platform: String, - pub deviceSubject: String, - pub algorithm: String, - pub publicKey: String, + name: String, + platform: String, + deviceSubject: String, + algorithm: String, + publicKey: String, +} + +impl DeviceRegistrationRequest { + pub fn new(name: String, platform: String, deviceSubject: String, algorithm: String, publicKey: String) -> Result { + let value = Self { name, platform, deviceSubject, algorithm, publicKey }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.name.is_empty() { return Err(ValidationError::new("name", "required")); } + if !self.name.is_empty() && self.name.len() < 1 { return Err(ValidationError::new("name", "min_length")); } + if self.name.len() > 128 { return Err(ValidationError::new("name", "max_length")); } + if self.platform.is_empty() { return Err(ValidationError::new("platform", "required")); } + if !self.platform.is_empty() && self.platform.len() < 1 { return Err(ValidationError::new("platform", "min_length")); } + if self.platform.len() > 64 { return Err(ValidationError::new("platform", "max_length")); } + if self.deviceSubject.is_empty() { return Err(ValidationError::new("device_subject", "required")); } + if !self.deviceSubject.is_empty() && self.deviceSubject.len() < 1 { return Err(ValidationError::new("device_subject", "min_length")); } + if self.deviceSubject.len() > 256 { return Err(ValidationError::new("device_subject", "max_length")); } + if self.algorithm != "ed25519" { return Err(ValidationError::new("algorithm", "invalid_value")); } + if self.publicKey.is_empty() { return Err(ValidationError::new("public_key", "required")); } + if !self.publicKey.is_empty() && self.publicKey.len() < 1 { return Err(ValidationError::new("public_key", "min_length")); } + if self.publicKey.len() > 256 { return Err(ValidationError::new("public_key", "max_length")); } + Ok(()) + } + pub fn name(&self) -> &String { &self.name } + pub fn platform(&self) -> &String { &self.platform } + pub fn deviceSubject(&self) -> &String { &self.deviceSubject } + pub fn algorithm(&self) -> &String { &self.algorithm } + pub fn publicKey(&self) -> &String { &self.publicKey } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct EntitledPool { - pub poolId: String, - pub name: String, - pub assignmentState: String, + poolId: String, + name: String, + assignmentState: String, +} + +impl EntitledPool { + pub fn new(poolId: String, name: String, assignmentState: String) -> Result { + let value = Self { poolId, name, assignmentState }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.poolId.is_empty() { return Err(ValidationError::new("pool_id", "required")); } + if !self.poolId.is_empty() && self.poolId.len() < 1 { return Err(ValidationError::new("pool_id", "min_length")); } + if self.poolId.len() > 128 { return Err(ValidationError::new("pool_id", "max_length")); } + if self.name.is_empty() { return Err(ValidationError::new("name", "required")); } + if !self.name.is_empty() && self.name.len() < 1 { return Err(ValidationError::new("name", "min_length")); } + if self.name.len() > 256 { return Err(ValidationError::new("name", "max_length")); } + if self.assignmentState.is_empty() { return Err(ValidationError::new("assignment_state", "required")); } + if !self.assignmentState.is_empty() && self.assignmentState.len() < 1 { return Err(ValidationError::new("assignment_state", "min_length")); } + if self.assignmentState.len() > 64 { return Err(ValidationError::new("assignment_state", "max_length")); } + Ok(()) + } + pub fn poolId(&self) -> &String { &self.poolId } + pub fn name(&self) -> &String { &self.name } + pub fn assignmentState(&self) -> &String { &self.assignmentState } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorEnvelope { - pub status: bool, - pub error: String, - pub code: String, - pub message: String, - pub resolution: String, - pub requestId: String, - pub violations: Vec, + status: bool, + error: String, + code: String, + message: String, + resolution: String, + requestId: String, + violations: Vec, +} + +impl ErrorEnvelope { + pub fn new(status: bool, error: String, code: String, message: String, resolution: String, requestId: String, violations: Vec) -> Result { + let value = Self { status, error, code, message, resolution, requestId, violations }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.error.is_empty() { return Err(ValidationError::new("error", "required")); } + if !self.error.is_empty() && self.error.len() < 1 { return Err(ValidationError::new("error", "min_length")); } + if self.error.len() > 512 { return Err(ValidationError::new("error", "max_length")); } + if self.code.is_empty() { return Err(ValidationError::new("code", "required")); } + if !self.code.is_empty() && self.code.len() < 1 { return Err(ValidationError::new("code", "min_length")); } + if self.code.len() > 128 { return Err(ValidationError::new("code", "max_length")); } + if self.message.is_empty() { return Err(ValidationError::new("message", "required")); } + if !self.message.is_empty() && self.message.len() < 1 { return Err(ValidationError::new("message", "min_length")); } + if self.message.len() > 512 { return Err(ValidationError::new("message", "max_length")); } + if self.resolution.is_empty() { return Err(ValidationError::new("resolution", "required")); } + if !self.resolution.is_empty() && self.resolution.len() < 1 { return Err(ValidationError::new("resolution", "min_length")); } + if self.resolution.len() > 128 { return Err(ValidationError::new("resolution", "max_length")); } + if self.requestId.is_empty() { return Err(ValidationError::new("request_id", "required")); } + if !self.requestId.is_empty() && self.requestId.len() < 1 { return Err(ValidationError::new("request_id", "min_length")); } + if self.requestId.len() > 128 { return Err(ValidationError::new("request_id", "max_length")); } + if self.violations.len() > 16 { return Err(ValidationError::new("violations", "max_items")); } + for item in self.violations.iter() { item.validate().map_err(|_| ValidationError::new("violations", "invalid_item"))?; } + Ok(()) + } + pub fn status(&self) -> &bool { &self.status } + pub fn error(&self) -> &String { &self.error } + pub fn code(&self) -> &String { &self.code } + pub fn message(&self) -> &String { &self.message } + pub fn resolution(&self) -> &String { &self.resolution } + pub fn requestId(&self) -> &String { &self.requestId } + pub fn violations(&self) -> &Vec { &self.violations } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct EventEnvelope { - pub eventId: String, - pub sequence: i64, - pub typeValue: String, - pub version: i64, - pub resource: ResourceLink, - pub occurredAt: String, - pub correlationId: String, - pub payload: JsonObject, + eventId: String, + sequence: i64, + typeValue: String, + version: i64, + resource: ResourceLink, + occurredAt: String, + correlationId: String, + payload: JsonObject, +} + +impl EventEnvelope { + pub fn new(eventId: String, sequence: i64, typeValue: String, version: i64, resource: ResourceLink, occurredAt: String, correlationId: String, payload: JsonObject) -> Result { + let value = Self { eventId, sequence, typeValue, version, resource, occurredAt, correlationId, payload }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.eventId.is_empty() { return Err(ValidationError::new("event_id", "required")); } + if !self.eventId.is_empty() && self.eventId.len() < 1 { return Err(ValidationError::new("event_id", "min_length")); } + if self.eventId.len() > 128 { return Err(ValidationError::new("event_id", "max_length")); } + if self.sequence < 1 { return Err(ValidationError::new("sequence", "minimum")); } + if self.typeValue.is_empty() { return Err(ValidationError::new("type", "required")); } + if !self.typeValue.is_empty() && self.typeValue.len() < 1 { return Err(ValidationError::new("type", "min_length")); } + if self.typeValue.len() > 128 { return Err(ValidationError::new("type", "max_length")); } + if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } + self.resource.validate().map_err(|_| ValidationError::new("resource", "invalid_object"))?; + if self.occurredAt.len() > 64 { return Err(ValidationError::new("occurred_at", "max_length")); } + if self.correlationId.is_empty() { return Err(ValidationError::new("correlation_id", "required")); } + if !self.correlationId.is_empty() && self.correlationId.len() < 1 { return Err(ValidationError::new("correlation_id", "min_length")); } + if self.correlationId.len() > 128 { return Err(ValidationError::new("correlation_id", "max_length")); } + Ok(()) + } + pub fn eventId(&self) -> &String { &self.eventId } + pub fn sequence(&self) -> &i64 { &self.sequence } + pub fn typeValue(&self) -> &String { &self.typeValue } + pub fn version(&self) -> &i64 { &self.version } + pub fn resource(&self) -> &ResourceLink { &self.resource } + pub fn occurredAt(&self) -> &String { &self.occurredAt } + pub fn correlationId(&self) -> &String { &self.correlationId } + pub fn payload(&self) -> &JsonObject { &self.payload } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct EventResume { - pub cursor: String, - pub lastSequence: i64, + cursor: String, + lastSequence: i64, +} + +impl EventResume { + pub fn new(cursor: String, lastSequence: i64) -> Result { + let value = Self { cursor, lastSequence }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.cursor.len() > 512 { return Err(ValidationError::new("cursor", "max_length")); } + if self.lastSequence < 0 { return Err(ValidationError::new("last_sequence", "minimum")); } + Ok(()) + } + pub fn cursor(&self) -> &String { &self.cursor } + pub fn lastSequence(&self) -> &i64 { &self.lastSequence } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldViolation { - pub field: String, - pub code: String, + field: String, + code: String, +} + +impl FieldViolation { + pub fn new(field: String, code: String) -> Result { + let value = Self { field, code }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.field.is_empty() { return Err(ValidationError::new("field", "required")); } + if !self.field.is_empty() && self.field.len() < 1 { return Err(ValidationError::new("field", "min_length")); } + if self.field.len() > 128 { return Err(ValidationError::new("field", "max_length")); } + if self.code.is_empty() { return Err(ValidationError::new("code", "required")); } + if !self.code.is_empty() && self.code.len() < 1 { return Err(ValidationError::new("code", "min_length")); } + if self.code.len() > 64 { return Err(ValidationError::new("code", "max_length")); } + Ok(()) + } + pub fn field(&self) -> &String { &self.field } + pub fn code(&self) -> &String { &self.code } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GatewayDrain { - pub version: String, - pub gatewayId: String, - pub sequence: i64, - pub reason: String, - pub deadline: String, + version: String, + gatewayId: String, + sequence: i64, + reason: String, + deadline: String, +} + +impl GatewayDrain { + pub fn new(version: String, gatewayId: String, sequence: i64, reason: String, deadline: String) -> Result { + let value = Self { version, gatewayId, sequence, reason, deadline }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); } + if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); } + if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } + if self.sequence < 1 { return Err(ValidationError::new("sequence", "minimum")); } + if self.reason.is_empty() { return Err(ValidationError::new("reason", "required")); } + if !self.reason.is_empty() && self.reason.len() < 1 { return Err(ValidationError::new("reason", "min_length")); } + if self.reason.len() > 256 { return Err(ValidationError::new("reason", "max_length")); } + if self.deadline.len() > 64 { return Err(ValidationError::new("deadline", "max_length")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn sequence(&self) -> &i64 { &self.sequence } + pub fn reason(&self) -> &String { &self.reason } + pub fn deadline(&self) -> &String { &self.deadline } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GatewayHeartbeat { - pub version: String, - pub gatewayId: String, - pub sequence: i64, - pub observedAt: String, - pub activeConnections: i64, - pub egressKbps: i64, - pub state: String, + version: String, + gatewayId: String, + sequence: i64, + observedAt: String, + activeConnections: i64, + egressKbps: i64, + state: String, +} + +impl GatewayHeartbeat { + pub fn new(version: String, gatewayId: String, sequence: i64, observedAt: String, activeConnections: i64, egressKbps: i64, state: String) -> Result { + let value = Self { version, gatewayId, sequence, observedAt, activeConnections, egressKbps, state }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); } + if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); } + if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } + if self.sequence < 1 { return Err(ValidationError::new("sequence", "minimum")); } + if self.observedAt.len() > 64 { return Err(ValidationError::new("observed_at", "max_length")); } + if self.activeConnections < 0 { return Err(ValidationError::new("active_connections", "minimum")); } + if self.activeConnections > 1000000 { return Err(ValidationError::new("active_connections", "maximum")); } + if self.egressKbps < 0 { return Err(ValidationError::new("egress_kbps", "minimum")); } + if self.egressKbps > 1000000000 { return Err(ValidationError::new("egress_kbps", "maximum")); } + if self.state != "ready" && self.state != "draining" && self.state != "offline" { return Err(ValidationError::new("state", "invalid_value")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn sequence(&self) -> &i64 { &self.sequence } + pub fn observedAt(&self) -> &String { &self.observedAt } + pub fn activeConnections(&self) -> &i64 { &self.activeConnections } + pub fn egressKbps(&self) -> &i64 { &self.egressKbps } + pub fn state(&self) -> &String { &self.state } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GatewayRegistration { - pub version: String, - pub gatewayId: String, - pub instanceIdentity: String, - pub certificateIdentity: String, - pub publicIdentity: String, - pub address: String, - pub providerIdentity: String, - pub protocolMinVersion: i64, - pub protocolMaxVersion: i64, - pub connectionCapacity: i64, - pub bandwidthCapacityKbps: i64, - pub features: Vec, - pub capabilities: CapabilityProfile, + version: String, + gatewayId: String, + instanceIdentity: String, + certificateIdentity: String, + publicIdentity: String, + address: String, + providerIdentity: String, + protocolMinVersion: i64, + protocolMaxVersion: i64, + connectionCapacity: i64, + bandwidthCapacityKbps: i64, + features: Vec, + capabilities: CapabilityProfile, +} + +impl GatewayRegistration { + pub fn new(version: String, gatewayId: String, instanceIdentity: String, certificateIdentity: String, publicIdentity: String, address: String, providerIdentity: String, protocolMinVersion: i64, protocolMaxVersion: i64, connectionCapacity: i64, bandwidthCapacityKbps: i64, features: Vec, capabilities: CapabilityProfile) -> Result { + let value = Self { version, gatewayId, instanceIdentity, certificateIdentity, publicIdentity, address, providerIdentity, protocolMinVersion, protocolMaxVersion, connectionCapacity, bandwidthCapacityKbps, features, capabilities }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); } + if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); } + if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } + if self.instanceIdentity.is_empty() { return Err(ValidationError::new("instance_identity", "required")); } + if !self.instanceIdentity.is_empty() && self.instanceIdentity.len() < 1 { return Err(ValidationError::new("instance_identity", "min_length")); } + if self.instanceIdentity.len() > 512 { return Err(ValidationError::new("instance_identity", "max_length")); } + if self.certificateIdentity.is_empty() { return Err(ValidationError::new("certificate_identity", "required")); } + if !self.certificateIdentity.is_empty() && self.certificateIdentity.len() < 1 { return Err(ValidationError::new("certificate_identity", "min_length")); } + if self.certificateIdentity.len() > 512 { return Err(ValidationError::new("certificate_identity", "max_length")); } + if self.publicIdentity.is_empty() { return Err(ValidationError::new("public_identity", "required")); } + if !self.publicIdentity.is_empty() && self.publicIdentity.len() < 1 { return Err(ValidationError::new("public_identity", "min_length")); } + if self.publicIdentity.len() > 256 { return Err(ValidationError::new("public_identity", "max_length")); } + if self.address.is_empty() { return Err(ValidationError::new("address", "required")); } + if !self.address.is_empty() && self.address.len() < 1 { return Err(ValidationError::new("address", "min_length")); } + if self.address.len() > 256 { return Err(ValidationError::new("address", "max_length")); } + if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); } + if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); } + if self.providerIdentity.len() > 256 { return Err(ValidationError::new("provider_identity", "max_length")); } + if self.protocolMinVersion < 1 { return Err(ValidationError::new("protocol_min_version", "minimum")); } + if self.protocolMinVersion > 100 { return Err(ValidationError::new("protocol_min_version", "maximum")); } + if self.protocolMaxVersion < 1 { return Err(ValidationError::new("protocol_max_version", "minimum")); } + if self.protocolMaxVersion > 100 { return Err(ValidationError::new("protocol_max_version", "maximum")); } + if self.connectionCapacity < 1 { return Err(ValidationError::new("connection_capacity", "minimum")); } + if self.connectionCapacity > 1000000 { return Err(ValidationError::new("connection_capacity", "maximum")); } + if self.bandwidthCapacityKbps < 1 { return Err(ValidationError::new("bandwidth_capacity_kbps", "minimum")); } + if self.bandwidthCapacityKbps > 1000000000 { return Err(ValidationError::new("bandwidth_capacity_kbps", "maximum")); } + if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); } + self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?; + if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new("protocol_version", "invalid_order")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn instanceIdentity(&self) -> &String { &self.instanceIdentity } + pub fn certificateIdentity(&self) -> &String { &self.certificateIdentity } + pub fn publicIdentity(&self) -> &String { &self.publicIdentity } + pub fn address(&self) -> &String { &self.address } + pub fn providerIdentity(&self) -> &String { &self.providerIdentity } + pub fn protocolMinVersion(&self) -> &i64 { &self.protocolMinVersion } + pub fn protocolMaxVersion(&self) -> &i64 { &self.protocolMaxVersion } + pub fn connectionCapacity(&self) -> &i64 { &self.connectionCapacity } + pub fn bandwidthCapacityKbps(&self) -> &i64 { &self.bandwidthCapacityKbps } + pub fn features(&self) -> &Vec { &self.features } + pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrantReference { - pub opaqueValue: String, - pub expiresAt: String, - pub audience: String, + opaqueValue: String, + expiresAt: String, + audience: String, +} + +impl GrantReference { + pub fn new(opaqueValue: String, expiresAt: String, audience: String) -> Result { + let value = Self { opaqueValue, expiresAt, audience }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.opaqueValue.is_empty() { return Err(ValidationError::new("opaque_value", "required")); } + if !self.opaqueValue.is_empty() && self.opaqueValue.len() < 43 { return Err(ValidationError::new("opaque_value", "min_length")); } + if self.opaqueValue.len() > 256 { return Err(ValidationError::new("opaque_value", "max_length")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); } + if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); } + if self.audience.len() > 128 { return Err(ValidationError::new("audience", "max_length")); } + Ok(()) + } + pub fn opaqueValue(&self) -> &String { &self.opaqueValue } + pub fn expiresAt(&self) -> &String { &self.expiresAt } + pub fn audience(&self) -> &String { &self.audience } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoginRequest { - pub provider: Option, - pub username: String, - pub password: String, + provider: Option, + username: String, + password: String, +} + +impl LoginRequest { + pub fn new(provider: Option, username: String, password: String) -> Result { + let value = Self { provider, username, password }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if let Some(value) = &self.provider { + if value != "ldap" && value != "local" { return Err(ValidationError::new("provider", "invalid_value")); } + } + if self.username.is_empty() { return Err(ValidationError::new("username", "required")); } + if !self.username.is_empty() && self.username.len() < 1 { return Err(ValidationError::new("username", "min_length")); } + if self.username.len() > 256 { return Err(ValidationError::new("username", "max_length")); } + if self.password.is_empty() { return Err(ValidationError::new("password", "required")); } + if !self.password.is_empty() && self.password.len() < 1 { return Err(ValidationError::new("password", "min_length")); } + if self.password.len() > 1024 { return Err(ValidationError::new("password", "max_length")); } + Ok(()) + } + pub fn provider(&self) -> &Option { &self.provider } + pub fn username(&self) -> &String { &self.username } + pub fn password(&self) -> &String { &self.password } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManifestBounds { - pub minimumKbps: i64, - pub targetKbps: i64, - pub maximumKbps: i64, + minimumKbps: i64, + targetKbps: i64, + maximumKbps: i64, +} + +impl ManifestBounds { + pub fn new(minimumKbps: i64, targetKbps: i64, maximumKbps: i64) -> Result { + let value = Self { minimumKbps, targetKbps, maximumKbps }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.minimumKbps < 1 { return Err(ValidationError::new("minimum_kbps", "minimum")); } + if self.minimumKbps > 100000000 { return Err(ValidationError::new("minimum_kbps", "maximum")); } + if self.targetKbps < 1 { return Err(ValidationError::new("target_kbps", "minimum")); } + if self.targetKbps > 100000000 { return Err(ValidationError::new("target_kbps", "maximum")); } + if self.maximumKbps < 1 { return Err(ValidationError::new("maximum_kbps", "minimum")); } + if self.maximumKbps > 100000000 { return Err(ValidationError::new("maximum_kbps", "maximum")); } + if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new("bounds", "invalid_order")); } + Ok(()) + } + pub fn minimumKbps(&self) -> &i64 { &self.minimumKbps } + pub fn targetKbps(&self) -> &i64 { &self.targetKbps } + pub fn maximumKbps(&self) -> &i64 { &self.maximumKbps } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManifestGateway { - pub id: String, - pub addresses: Vec, - pub publicIdentity: String, + id: String, + addresses: Vec, + publicIdentity: String, +} + +impl ManifestGateway { + pub fn new(id: String, addresses: Vec, publicIdentity: String) -> Result { + let value = Self { id, addresses, publicIdentity }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.id.is_empty() { return Err(ValidationError::new("id", "required")); } + if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } + if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } + if self.addresses.len() < 1 { return Err(ValidationError::new("addresses", "min_items")); } + if self.addresses.len() > 4 { return Err(ValidationError::new("addresses", "max_items")); } + if self.publicIdentity.is_empty() { return Err(ValidationError::new("public_identity", "required")); } + if !self.publicIdentity.is_empty() && self.publicIdentity.len() < 1 { return Err(ValidationError::new("public_identity", "min_length")); } + if self.publicIdentity.len() > 256 { return Err(ValidationError::new("public_identity", "max_length")); } + Ok(()) + } + pub fn id(&self) -> &String { &self.id } + pub fn addresses(&self) -> &Vec { &self.addresses } + pub fn publicIdentity(&self) -> &String { &self.publicIdentity } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManifestProfile { - pub id: String, - pub bounds: ManifestBounds, + id: String, + bounds: ManifestBounds, +} + +impl ManifestProfile { + pub fn new(id: String, bounds: ManifestBounds) -> Result { + let value = Self { id, bounds }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.id.is_empty() { return Err(ValidationError::new("id", "required")); } + if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } + if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } + self.bounds.validate().map_err(|_| ValidationError::new("bounds", "invalid_object"))?; + Ok(()) + } + pub fn id(&self) -> &String { &self.id } + pub fn bounds(&self) -> &ManifestBounds { &self.bounds } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManifestTunnel { - pub versions: Vec, - pub features: Vec, + versions: Vec, + features: Vec, +} + +impl ManifestTunnel { + pub fn new(versions: Vec, features: Vec) -> Result { + let value = Self { versions, features }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.versions.len() < 1 { return Err(ValidationError::new("versions", "min_items")); } + if self.versions.len() > 4 { return Err(ValidationError::new("versions", "max_items")); } + if self.features.len() > 32 { return Err(ValidationError::new("features", "max_items")); } + Ok(()) + } + pub fn versions(&self) -> &Vec { &self.versions } + pub fn features(&self) -> &Vec { &self.features } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct NativeCredential { - pub deviceId: Option, - pub familyId: String, - pub accessToken: String, - pub refreshToken: String, - pub expiresAt: String, - pub refreshExpiresAt: Option, + deviceId: Option, + familyId: String, + accessToken: String, + refreshToken: String, + expiresAt: String, + refreshExpiresAt: Option, +} + +impl NativeCredential { + pub fn new(deviceId: Option, familyId: String, accessToken: String, refreshToken: String, expiresAt: String, refreshExpiresAt: Option) -> Result { + let value = Self { deviceId, familyId, accessToken, refreshToken, expiresAt, refreshExpiresAt }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if let Some(value) = &self.deviceId { + if value.len() > 128 { return Err(ValidationError::new("device_id", "max_length")); } + } + if self.familyId.is_empty() { return Err(ValidationError::new("family_id", "required")); } + if !self.familyId.is_empty() && self.familyId.len() < 1 { return Err(ValidationError::new("family_id", "min_length")); } + if self.familyId.len() > 128 { return Err(ValidationError::new("family_id", "max_length")); } + if self.accessToken.is_empty() { return Err(ValidationError::new("access_token", "required")); } + if !self.accessToken.is_empty() && self.accessToken.len() < 1 { return Err(ValidationError::new("access_token", "min_length")); } + if self.accessToken.len() > 256 { return Err(ValidationError::new("access_token", "max_length")); } + if self.refreshToken.is_empty() { return Err(ValidationError::new("refresh_token", "required")); } + if !self.refreshToken.is_empty() && self.refreshToken.len() < 1 { return Err(ValidationError::new("refresh_token", "min_length")); } + if self.refreshToken.len() > 256 { return Err(ValidationError::new("refresh_token", "max_length")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + if let Some(value) = &self.refreshExpiresAt { + if value.len() > 64 { return Err(ValidationError::new("refresh_expires_at", "max_length")); } + } + Ok(()) + } + pub fn deviceId(&self) -> &Option { &self.deviceId } + pub fn familyId(&self) -> &String { &self.familyId } + pub fn accessToken(&self) -> &String { &self.accessToken } + pub fn refreshToken(&self) -> &String { &self.refreshToken } + pub fn expiresAt(&self) -> &String { &self.expiresAt } + pub fn refreshExpiresAt(&self) -> &Option { &self.refreshExpiresAt } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PageInfo { - pub limit: i64, - pub nextCursor: String, + limit: i64, + nextCursor: String, +} + +impl PageInfo { + pub fn new(limit: i64, nextCursor: String) -> Result { + let value = Self { limit, nextCursor }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.limit < 1 { return Err(ValidationError::new("limit", "minimum")); } + if self.limit > 100 { return Err(ValidationError::new("limit", "maximum")); } + if self.nextCursor.len() > 512 { return Err(ValidationError::new("next_cursor", "max_length")); } + Ok(()) + } + pub fn limit(&self) -> &i64 { &self.limit } + pub fn nextCursor(&self) -> &String { &self.nextCursor } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderState { - pub version: String, - pub sessionId: String, - pub state: String, - pub cleanupPending: bool, - pub channels: Vec, + version: String, + sessionId: String, + state: String, + cleanupPending: bool, + channels: Vec, +} + +impl ProviderState { + pub fn new(version: String, sessionId: String, state: String, cleanupPending: bool, channels: Vec) -> Result { + let value = Self { version, sessionId, state, cleanupPending, channels }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); } + if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); } + if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); } + if self.state != "starting" && self.state != "ready" && self.state != "disconnected" && self.state != "terminating" && self.state != "terminated" && self.state != "cleanup_pending" && self.state != "failed" { return Err(ValidationError::new("state", "invalid_value")); } + if self.channels.len() > 8 { return Err(ValidationError::new("channels", "max_items")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn sessionId(&self) -> &String { &self.sessionId } + pub fn state(&self) -> &String { &self.state } + pub fn cleanupPending(&self) -> &bool { &self.cleanupPending } + pub fn channels(&self) -> &Vec { &self.channels } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReauthGrant { - pub token: String, - pub purpose: String, - pub expiresAt: String, + token: String, + purpose: String, + expiresAt: String, +} + +impl ReauthGrant { + pub fn new(token: String, purpose: String, expiresAt: String) -> Result { + let value = Self { token, purpose, expiresAt }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.token.is_empty() { return Err(ValidationError::new("token", "required")); } + if !self.token.is_empty() && self.token.len() < 1 { return Err(ValidationError::new("token", "min_length")); } + if self.token.len() > 256 { return Err(ValidationError::new("token", "max_length")); } + if self.purpose.is_empty() { return Err(ValidationError::new("purpose", "required")); } + if !self.purpose.is_empty() && self.purpose.len() < 1 { return Err(ValidationError::new("purpose", "min_length")); } + if self.purpose.len() > 64 { return Err(ValidationError::new("purpose", "max_length")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + Ok(()) + } + pub fn token(&self) -> &String { &self.token } + pub fn purpose(&self) -> &String { &self.purpose } + pub fn expiresAt(&self) -> &String { &self.expiresAt } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReauthRequest { - pub password: String, - pub purpose: String, + password: String, + purpose: String, +} + +impl ReauthRequest { + pub fn new(password: String, purpose: String) -> Result { + let value = Self { password, purpose }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.password.is_empty() { return Err(ValidationError::new("password", "required")); } + if !self.password.is_empty() && self.password.len() < 1 { return Err(ValidationError::new("password", "min_length")); } + if self.password.len() > 1024 { return Err(ValidationError::new("password", "max_length")); } + if self.purpose != "identity_change" && self.purpose != "key_change" && self.purpose != "backup_enable" && self.purpose != "external_database_tls_disabled" && self.purpose != "assignment_change" { return Err(ValidationError::new("purpose", "invalid_value")); } + Ok(()) + } + pub fn password(&self) -> &String { &self.password } + pub fn purpose(&self) -> &String { &self.purpose } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReconnectRequest { - pub clientDeviceId: String, - pub deviceKeyId: String, - pub expectedVersion: i64, + clientDeviceId: String, + deviceKeyId: String, + expectedVersion: i64, +} + +impl ReconnectRequest { + pub fn new(clientDeviceId: String, deviceKeyId: String, expectedVersion: i64) -> Result { + let value = Self { clientDeviceId, deviceKeyId, expectedVersion }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.clientDeviceId.is_empty() { return Err(ValidationError::new("client_device_id", "required")); } + if !self.clientDeviceId.is_empty() && self.clientDeviceId.len() < 1 { return Err(ValidationError::new("client_device_id", "min_length")); } + if self.clientDeviceId.len() > 128 { return Err(ValidationError::new("client_device_id", "max_length")); } + if self.deviceKeyId.is_empty() { return Err(ValidationError::new("device_key_id", "required")); } + if !self.deviceKeyId.is_empty() && self.deviceKeyId.len() < 1 { return Err(ValidationError::new("device_key_id", "min_length")); } + if self.deviceKeyId.len() > 128 { return Err(ValidationError::new("device_key_id", "max_length")); } + if self.expectedVersion < 1 { return Err(ValidationError::new("expected_version", "minimum")); } + Ok(()) + } + pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId } + pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId } + pub fn expectedVersion(&self) -> &i64 { &self.expectedVersion } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RefreshRequest { - pub familyId: String, - pub refreshToken: String, + familyId: String, + refreshToken: String, +} + +impl RefreshRequest { + pub fn new(familyId: String, refreshToken: String) -> Result { + let value = Self { familyId, refreshToken }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.familyId.is_empty() { return Err(ValidationError::new("family_id", "required")); } + if !self.familyId.is_empty() && self.familyId.len() < 1 { return Err(ValidationError::new("family_id", "min_length")); } + if self.familyId.len() > 128 { return Err(ValidationError::new("family_id", "max_length")); } + if self.refreshToken.is_empty() { return Err(ValidationError::new("refresh_token", "required")); } + if !self.refreshToken.is_empty() && self.refreshToken.len() < 1 { return Err(ValidationError::new("refresh_token", "min_length")); } + if self.refreshToken.len() > 256 { return Err(ValidationError::new("refresh_token", "max_length")); } + Ok(()) + } + pub fn familyId(&self) -> &String { &self.familyId } + pub fn refreshToken(&self) -> &String { &self.refreshToken } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Resource { - pub id: String, - pub kind: String, - pub name: String, - pub state: String, - pub assignmentState: Option, - pub version: i64, - pub links: Vec, + id: String, + kind: String, + name: String, + state: String, + assignmentState: Option, + version: i64, + links: Vec, +} + +impl Resource { + pub fn new(id: String, kind: String, name: String, state: String, assignmentState: Option, version: i64, links: Vec) -> Result { + let value = Self { id, kind, name, state, assignmentState, version, links }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.id.is_empty() { return Err(ValidationError::new("id", "required")); } + if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } + if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } + if self.kind.is_empty() { return Err(ValidationError::new("kind", "required")); } + if !self.kind.is_empty() && self.kind.len() < 1 { return Err(ValidationError::new("kind", "min_length")); } + if self.kind.len() > 64 { return Err(ValidationError::new("kind", "max_length")); } + if self.name.is_empty() { return Err(ValidationError::new("name", "required")); } + if !self.name.is_empty() && self.name.len() < 1 { return Err(ValidationError::new("name", "min_length")); } + if self.name.len() > 256 { return Err(ValidationError::new("name", "max_length")); } + if self.state.is_empty() { return Err(ValidationError::new("state", "required")); } + if !self.state.is_empty() && self.state.len() < 1 { return Err(ValidationError::new("state", "min_length")); } + if self.state.len() > 64 { return Err(ValidationError::new("state", "max_length")); } + if let Some(value) = &self.assignmentState { + if value.len() > 64 { return Err(ValidationError::new("assignment_state", "max_length")); } + } + if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } + if self.links.len() > 16 { return Err(ValidationError::new("links", "max_items")); } + for item in self.links.iter() { item.validate().map_err(|_| ValidationError::new("links", "invalid_item"))?; } + Ok(()) + } + pub fn id(&self) -> &String { &self.id } + pub fn kind(&self) -> &String { &self.kind } + pub fn name(&self) -> &String { &self.name } + pub fn state(&self) -> &String { &self.state } + pub fn assignmentState(&self) -> &Option { &self.assignmentState } + pub fn version(&self) -> &i64 { &self.version } + pub fn links(&self) -> &Vec { &self.links } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResourceLink { - pub typeValue: String, - pub id: String, - pub version: i64, + typeValue: String, + id: String, + version: i64, +} + +impl ResourceLink { + pub fn new(typeValue: String, id: String, version: i64) -> Result { + let value = Self { typeValue, id, version }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.typeValue.is_empty() { return Err(ValidationError::new("type", "required")); } + if !self.typeValue.is_empty() && self.typeValue.len() < 1 { return Err(ValidationError::new("type", "min_length")); } + if self.typeValue.len() > 64 { return Err(ValidationError::new("type", "max_length")); } + if self.id.is_empty() { return Err(ValidationError::new("id", "required")); } + if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } + if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } + if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } + Ok(()) + } + pub fn typeValue(&self) -> &String { &self.typeValue } + pub fn id(&self) -> &String { &self.id } + pub fn version(&self) -> &i64 { &self.version } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResourceList { - pub assignedDesktops: Vec, - pub entitledPools: Vec, - pub page: PageInfo, + assignedDesktops: Vec, + entitledPools: Vec, + page: PageInfo, +} + +impl ResourceList { + pub fn new(assignedDesktops: Vec, entitledPools: Vec, page: PageInfo) -> Result { + let value = Self { assignedDesktops, entitledPools, page }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.assignedDesktops.len() > 100 { return Err(ValidationError::new("assigned_desktops", "max_items")); } + for item in self.assignedDesktops.iter() { item.validate().map_err(|_| ValidationError::new("assigned_desktops", "invalid_item"))?; } + if self.entitledPools.len() > 100 { return Err(ValidationError::new("entitled_pools", "max_items")); } + for item in self.entitledPools.iter() { item.validate().map_err(|_| ValidationError::new("entitled_pools", "invalid_item"))?; } + self.page.validate().map_err(|_| ValidationError::new("page", "invalid_object"))?; + Ok(()) + } + pub fn assignedDesktops(&self) -> &Vec { &self.assignedDesktops } + pub fn entitledPools(&self) -> &Vec { &self.entitledPools } + pub fn page(&self) -> &PageInfo { &self.page } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionAuthority { - pub version: String, - pub sessionId: String, - pub gatewayId: String, - pub audience: String, - pub reconnectSequence: i64, - pub expiresAt: String, - pub capabilities: CapabilityProfile, - pub providerProfile: String, - pub providerIdentity: String, + version: String, + sessionId: String, + gatewayId: String, + audience: String, + reconnectSequence: i64, + expiresAt: String, + capabilities: CapabilityProfile, + providerProfile: String, + providerIdentity: String, +} + +impl SessionAuthority { + pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, reconnectSequence: i64, expiresAt: String, capabilities: CapabilityProfile, providerProfile: String, providerIdentity: String) -> Result { + let value = Self { version, sessionId, gatewayId, audience, reconnectSequence, expiresAt, capabilities, providerProfile, providerIdentity }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); } + if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); } + if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); } + if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); } + if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); } + if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } + if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); } + if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); } + if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); } + if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?; + if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); } + if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); } + if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); } + if self.providerIdentity.len() > 256 { return Err(ValidationError::new("provider_identity", "max_length")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn sessionId(&self) -> &String { &self.sessionId } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn audience(&self) -> &String { &self.audience } + pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence } + pub fn expiresAt(&self) -> &String { &self.expiresAt } + pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities } + pub fn providerProfile(&self) -> &String { &self.providerProfile } + pub fn providerIdentity(&self) -> &String { &self.providerIdentity } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionRequest { - pub clientDeviceId: String, - pub deviceKeyId: String, - pub poolId: String, - pub idempotencyKey: String, - pub policySnapshot: AllocationPolicy, + clientDeviceId: String, + deviceKeyId: String, + poolId: String, + idempotencyKey: String, + policySnapshot: AllocationPolicy, +} + +impl SessionRequest { + pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) -> Result { + let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, policySnapshot }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.clientDeviceId.is_empty() { return Err(ValidationError::new("client_device_id", "required")); } + if !self.clientDeviceId.is_empty() && self.clientDeviceId.len() < 1 { return Err(ValidationError::new("client_device_id", "min_length")); } + if self.clientDeviceId.len() > 128 { return Err(ValidationError::new("client_device_id", "max_length")); } + if self.deviceKeyId.is_empty() { return Err(ValidationError::new("device_key_id", "required")); } + if !self.deviceKeyId.is_empty() && self.deviceKeyId.len() < 1 { return Err(ValidationError::new("device_key_id", "min_length")); } + if self.deviceKeyId.len() > 128 { return Err(ValidationError::new("device_key_id", "max_length")); } + if self.poolId.is_empty() { return Err(ValidationError::new("pool_id", "required")); } + if !self.poolId.is_empty() && self.poolId.len() < 1 { return Err(ValidationError::new("pool_id", "min_length")); } + if self.poolId.len() > 128 { return Err(ValidationError::new("pool_id", "max_length")); } + if self.idempotencyKey.is_empty() { return Err(ValidationError::new("idempotency_key", "required")); } + if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); } + if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); } + self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?; + Ok(()) + } + pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId } + pub fn deviceKeyId(&self) -> &String { &self.deviceKeyId } + pub fn poolId(&self) -> &String { &self.poolId } + pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey } + pub fn policySnapshot(&self) -> &AllocationPolicy { &self.policySnapshot } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct StableError { - pub version: String, - pub code: String, - pub message: String, - pub retryable: bool, + version: String, + code: String, + message: String, + retryable: bool, +} + +impl StableError { + pub fn new(version: String, code: String, message: String, retryable: bool) -> Result { + let value = Self { version, code, message, retryable }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.code.is_empty() { return Err(ValidationError::new("code", "required")); } + if !self.code.is_empty() && self.code.len() < 1 { return Err(ValidationError::new("code", "min_length")); } + if self.code.len() > 128 { return Err(ValidationError::new("code", "max_length")); } + if self.message.is_empty() { return Err(ValidationError::new("message", "required")); } + if !self.message.is_empty() && self.message.len() < 1 { return Err(ValidationError::new("message", "min_length")); } + if self.message.len() > 512 { return Err(ValidationError::new("message", "max_length")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn code(&self) -> &String { &self.code } + pub fn message(&self) -> &String { &self.message } + pub fn retryable(&self) -> &bool { &self.retryable } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct TunnelAdmissionRequest { - pub version: String, - pub sessionId: String, - pub gatewayId: String, - pub audience: String, - pub grant: String, - pub reconnectSequence: i64, - pub clientNonce: String, - pub capabilities: CapabilityProfile, + version: String, + sessionId: String, + gatewayId: String, + audience: String, + grant: String, + reconnectSequence: i64, + clientNonce: String, + capabilities: CapabilityProfile, +} + +impl TunnelAdmissionRequest { + pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: i64, clientNonce: String, capabilities: CapabilityProfile) -> Result { + let value = Self { version, sessionId, gatewayId, audience, grant, reconnectSequence, clientNonce, capabilities }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); } + if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); } + if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); } + if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); } + if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); } + if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); } + if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); } + if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); } + if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); } + if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); } + if self.grant.is_empty() { return Err(ValidationError::new("grant", "required")); } + if !self.grant.is_empty() && self.grant.len() < 43 { return Err(ValidationError::new("grant", "min_length")); } + if self.grant.len() > 256 { return Err(ValidationError::new("grant", "max_length")); } + if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } + if self.clientNonce.is_empty() { return Err(ValidationError::new("client_nonce", "required")); } + if !self.clientNonce.is_empty() && self.clientNonce.len() < 16 { return Err(ValidationError::new("client_nonce", "min_length")); } + if self.clientNonce.len() > 128 { return Err(ValidationError::new("client_nonce", "max_length")); } + self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?; + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn sessionId(&self) -> &String { &self.sessionId } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn audience(&self) -> &String { &self.audience } + pub fn grant(&self) -> &String { &self.grant } + pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence } + pub fn clientNonce(&self) -> &String { &self.clientNonce } + pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct VersionNegotiation { - pub supportedVersions: Vec, - pub features: Vec, + supportedVersions: Vec, + features: Vec, +} + +impl VersionNegotiation { + pub fn new(supportedVersions: Vec, features: Vec) -> Result { + let value = Self { supportedVersions, features }; + value.validate()?; + Ok(value) + } + pub fn validate(&self) -> Result<(), ValidationError> { + if self.supportedVersions.len() < 1 { return Err(ValidationError::new("supported_versions", "min_items")); } + if self.supportedVersions.len() > 3 { return Err(ValidationError::new("supported_versions", "max_items")); } + if self.features.len() > 64 { return Err(ValidationError::new("features", "max_items")); } + Ok(()) + } + pub fn supportedVersions(&self) -> &Vec { &self.supportedVersions } + pub fn features(&self) -> &Vec { &self.features } +} + +pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result { + 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) } diff --git a/gen/swift/Protocol.swift b/gen/swift/Protocol.swift index 6e1af76..3a9f00d 100644 --- a/gen/swift/Protocol.swift +++ b/gen/swift/Protocol.swift @@ -5,6 +5,8 @@ public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e public let currentWireVersion = "1" public let nMinus1WireVersion = "0" public let nMinus2WireVersion = "-1" +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 } } public struct AllocationPolicy: Codable, Equatable { public let minimumKbps: Int64 @@ -28,18 +30,51 @@ public struct AllocationPolicy: Codable, Equatable { case reservationLeaseSeconds = "reservation_lease_seconds" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps) - targetKbps = try c.decode(Int64.self, forKey: .targetKbps) - maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps) - tier = try c.decode(String.self, forKey: .tier) - audience = try c.decode(String.self, forKey: .audience) - protocolValue = try c.decode(String.self, forKey: .protocolValue) - protocolVersion = try c.decode(Int64.self, forKey: .protocolVersion) - grantTtlSeconds = try c.decode(Int64.self, forKey: .grantTtlSeconds) - reservationLeaseSeconds = try c.decode(Int64.self, forKey: .reservationLeaseSeconds) + public init(minimumKbps: Int64, targetKbps: Int64, maximumKbps: Int64, tier: String, audience: String, protocolValue: String, protocolVersion: Int64, grantTtlSeconds: Int64, reservationLeaseSeconds: Int64) throws { + self.minimumKbps = minimumKbps + self.targetKbps = targetKbps + self.maximumKbps = maximumKbps + self.tier = tier + self.audience = audience + self.protocolValue = protocolValue + self.protocolVersion = protocolVersion + self.grantTtlSeconds = grantTtlSeconds + self.reservationLeaseSeconds = reservationLeaseSeconds + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(minimumKbps: try c.decode(Int64.self, forKey: .minimumKbps), targetKbps: try c.decode(Int64.self, forKey: .targetKbps), maximumKbps: try c.decode(Int64.self, forKey: .maximumKbps), tier: try c.decode(String.self, forKey: .tier), audience: try c.decode(String.self, forKey: .audience), protocolValue: try c.decode(String.self, forKey: .protocolValue), protocolVersion: try c.decode(Int64.self, forKey: .protocolVersion), grantTtlSeconds: try c.decode(Int64.self, forKey: .grantTtlSeconds), reservationLeaseSeconds: try c.decode(Int64.self, forKey: .reservationLeaseSeconds)) + } + + public func validate() throws { + if self.minimumKbps < 1 { throw ContractValidationError(field: "minimum_kbps", code: "minimum") } + if self.minimumKbps > 100000000 { throw ContractValidationError(field: "minimum_kbps", code: "maximum") } + if self.targetKbps < 1 { throw ContractValidationError(field: "target_kbps", code: "minimum") } + if self.targetKbps > 100000000 { throw ContractValidationError(field: "target_kbps", code: "maximum") } + if self.maximumKbps < 1 { throw ContractValidationError(field: "maximum_kbps", code: "minimum") } + if self.maximumKbps > 100000000 { throw ContractValidationError(field: "maximum_kbps", code: "maximum") } + if !["standard", "priority", "premium"].contains(self.tier) { throw ContractValidationError(field: "tier", code: "invalid_value") } + if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") } + if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") } + if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") } + if self.protocolValue.isEmpty { throw ContractValidationError(field: "protocol", code: "required") } + if !self.protocolValue.isEmpty && self.protocolValue.utf8.count < 1 { throw ContractValidationError(field: "protocol", code: "min_length") } + if self.protocolValue.utf8.count > 64 { throw ContractValidationError(field: "protocol", code: "max_length") } + if self.protocolVersion < 1 { throw ContractValidationError(field: "protocol_version", code: "minimum") } + if self.protocolVersion > 100 { throw ContractValidationError(field: "protocol_version", code: "maximum") } + if self.grantTtlSeconds < 5 { throw ContractValidationError(field: "grant_ttl_seconds", code: "minimum") } + if self.grantTtlSeconds > 300 { throw ContractValidationError(field: "grant_ttl_seconds", code: "maximum") } + if self.reservationLeaseSeconds < 5 { throw ContractValidationError(field: "reservation_lease_seconds", code: "minimum") } + if self.reservationLeaseSeconds > 3600 { throw ContractValidationError(field: "reservation_lease_seconds", code: "maximum") } + if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: "bounds", code: "invalid_order") } + } + + 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) } } public struct AssignedDesktop: Codable, Equatable { @@ -54,13 +89,38 @@ public struct AssignedDesktop: Codable, Equatable { case availability = "availability" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - assignmentId = try c.decode(String.self, forKey: .assignmentId) - poolId = try c.decode(String.self, forKey: .poolId) - name = try c.decode(String.self, forKey: .name) - availability = try c.decode(String.self, forKey: .availability) + public init(assignmentId: String, poolId: String, name: String, availability: String) throws { + self.assignmentId = assignmentId + self.poolId = poolId + self.name = name + self.availability = availability + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(assignmentId: try c.decode(String.self, forKey: .assignmentId), poolId: try c.decode(String.self, forKey: .poolId), name: try c.decode(String.self, forKey: .name), availability: try c.decode(String.self, forKey: .availability)) + } + + public func validate() throws { + if self.assignmentId.isEmpty { throw ContractValidationError(field: "assignment_id", code: "required") } + if !self.assignmentId.isEmpty && self.assignmentId.utf8.count < 1 { throw ContractValidationError(field: "assignment_id", code: "min_length") } + if self.assignmentId.utf8.count > 128 { throw ContractValidationError(field: "assignment_id", code: "max_length") } + if self.poolId.isEmpty { throw ContractValidationError(field: "pool_id", code: "required") } + if !self.poolId.isEmpty && self.poolId.utf8.count < 1 { throw ContractValidationError(field: "pool_id", code: "min_length") } + if self.poolId.utf8.count > 128 { throw ContractValidationError(field: "pool_id", code: "max_length") } + if self.name.isEmpty { throw ContractValidationError(field: "name", code: "required") } + if !self.name.isEmpty && self.name.utf8.count < 1 { throw ContractValidationError(field: "name", code: "min_length") } + if self.name.utf8.count > 256 { throw ContractValidationError(field: "name", code: "max_length") } + if self.availability.isEmpty { throw ContractValidationError(field: "availability", code: "required") } + if !self.availability.isEmpty && self.availability.utf8.count < 1 { throw ContractValidationError(field: "availability", code: "min_length") } + if self.availability.utf8.count > 64 { throw ContractValidationError(field: "availability", code: "max_length") } + } + + 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) } } public struct BrokerSession: Codable, Equatable { @@ -97,24 +157,79 @@ public struct BrokerSession: Codable, Equatable { case version = "version" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - id = try c.decode(String.self, forKey: .id) - principalId = try c.decode(String.self, forKey: .principalId) - poolId = try c.decode(String.self, forKey: .poolId) - assignmentId = try c.decodeIfPresent(String.self, forKey: .assignmentId) - state = try c.decode(String.self, forKey: .state) - policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot) - reconnectDeadline = try c.decodeIfPresent(String.self, forKey: .reconnectDeadline) - outcome = try c.decodeIfPresent(String.self, forKey: .outcome) - failureCode = try c.decodeIfPresent(String.self, forKey: .failureCode) - cleanupState = try c.decode(String.self, forKey: .cleanupState) - idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey) - correlationId = try c.decode(String.self, forKey: .correlationId) - requestedAt = try c.decode(String.self, forKey: .requestedAt) - endedAt = try c.decodeIfPresent(String.self, forKey: .endedAt) - version = try c.decode(Int64.self, forKey: .version) + public init(id: String, principalId: String, poolId: String, assignmentId: String?, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: String?, outcome: String?, failureCode: String?, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: String?, version: Int64) throws { + self.id = id + self.principalId = principalId + self.poolId = poolId + self.assignmentId = assignmentId + self.state = state + self.policySnapshot = policySnapshot + self.reconnectDeadline = reconnectDeadline + self.outcome = outcome + self.failureCode = failureCode + self.cleanupState = cleanupState + self.idempotencyKey = idempotencyKey + self.correlationId = correlationId + self.requestedAt = requestedAt + self.endedAt = endedAt + self.version = version + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(id: try c.decode(String.self, forKey: .id), principalId: try c.decode(String.self, forKey: .principalId), poolId: try c.decode(String.self, forKey: .poolId), assignmentId: try c.decodeIfPresent(String.self, forKey: .assignmentId), state: try c.decode(String.self, forKey: .state), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), reconnectDeadline: try c.decodeIfPresent(String.self, forKey: .reconnectDeadline), outcome: try c.decodeIfPresent(String.self, forKey: .outcome), failureCode: try c.decodeIfPresent(String.self, forKey: .failureCode), cleanupState: try c.decode(String.self, forKey: .cleanupState), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), correlationId: try c.decode(String.self, forKey: .correlationId), requestedAt: try c.decode(String.self, forKey: .requestedAt), endedAt: try c.decodeIfPresent(String.self, forKey: .endedAt), version: try c.decode(Int64.self, forKey: .version)) + } + + public func validate() throws { + if self.id.isEmpty { throw ContractValidationError(field: "id", code: "required") } + if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } + if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } + if self.principalId.isEmpty { throw ContractValidationError(field: "principal_id", code: "required") } + if !self.principalId.isEmpty && self.principalId.utf8.count < 1 { throw ContractValidationError(field: "principal_id", code: "min_length") } + if self.principalId.utf8.count > 128 { throw ContractValidationError(field: "principal_id", code: "max_length") } + if self.poolId.isEmpty { throw ContractValidationError(field: "pool_id", code: "required") } + if !self.poolId.isEmpty && self.poolId.utf8.count < 1 { throw ContractValidationError(field: "pool_id", code: "min_length") } + if self.poolId.utf8.count > 128 { throw ContractValidationError(field: "pool_id", code: "max_length") } + if let value = self.assignmentId { + if value.utf8.count > 128 { throw ContractValidationError(field: "assignment_id", code: "max_length") } + } + if self.state.isEmpty { throw ContractValidationError(field: "state", code: "required") } + if !self.state.isEmpty && self.state.utf8.count < 1 { throw ContractValidationError(field: "state", code: "min_length") } + if self.state.utf8.count > 64 { throw ContractValidationError(field: "state", code: "max_length") } + try self.policySnapshot.validate() + if let value = self.reconnectDeadline { + if value.utf8.count > 64 { throw ContractValidationError(field: "reconnect_deadline", code: "max_length") } + if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "reconnect_deadline", code: "invalid_time") } + } + if let value = self.outcome { + if value.utf8.count > 64 { throw ContractValidationError(field: "outcome", code: "max_length") } + } + if let value = self.failureCode { + if value.utf8.count > 128 { throw ContractValidationError(field: "failure_code", code: "max_length") } + } + if self.cleanupState.isEmpty { throw ContractValidationError(field: "cleanup_state", code: "required") } + if !self.cleanupState.isEmpty && self.cleanupState.utf8.count < 1 { throw ContractValidationError(field: "cleanup_state", code: "min_length") } + if self.cleanupState.utf8.count > 64 { throw ContractValidationError(field: "cleanup_state", code: "max_length") } + if self.idempotencyKey.isEmpty { throw ContractValidationError(field: "idempotency_key", code: "required") } + if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") } + if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") } + if self.correlationId.isEmpty { throw ContractValidationError(field: "correlation_id", code: "required") } + if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") } + if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") } + if self.requestedAt.utf8.count > 64 { throw ContractValidationError(field: "requested_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.requestedAt) == nil { throw ContractValidationError(field: "requested_at", code: "invalid_time") } + if let value = self.endedAt { + if value.utf8.count > 64 { throw ContractValidationError(field: "ended_at", code: "max_length") } + if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "ended_at", code: "invalid_time") } + } + if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } + } + + 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) } } public struct CapabilityProfile: Codable, Equatable { @@ -133,15 +248,46 @@ public struct CapabilityProfile: Codable, Equatable { case clientDecode = "client_decode" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - transport = try c.decode(String.self, forKey: .transport) - framing = try c.decode(String.self, forKey: .framing) - media = try c.decode(String.self, forKey: .media) - audio = try c.decode(String.self, forKey: .audio) - sourceRateControl = try c.decode(String.self, forKey: .sourceRateControl) - clientDecode = try c.decode(String.self, forKey: .clientDecode) + public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) throws { + self.transport = transport + self.framing = framing + self.media = media + self.audio = audio + self.sourceRateControl = sourceRateControl + self.clientDecode = clientDecode + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode(String.self, forKey: .clientDecode)) + } + + public func validate() throws { + if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") } + if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") } + if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") } + if self.framing.isEmpty { throw ContractValidationError(field: "framing", code: "required") } + if !self.framing.isEmpty && self.framing.utf8.count < 1 { throw ContractValidationError(field: "framing", code: "min_length") } + if self.framing.utf8.count > 64 { throw ContractValidationError(field: "framing", code: "max_length") } + if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") } + if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") } + if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") } + if self.audio.isEmpty { throw ContractValidationError(field: "audio", code: "required") } + if !self.audio.isEmpty && self.audio.utf8.count < 1 { throw ContractValidationError(field: "audio", code: "min_length") } + if self.audio.utf8.count > 64 { throw ContractValidationError(field: "audio", code: "max_length") } + if self.sourceRateControl.isEmpty { throw ContractValidationError(field: "source_rate_control", code: "required") } + if !self.sourceRateControl.isEmpty && self.sourceRateControl.utf8.count < 1 { throw ContractValidationError(field: "source_rate_control", code: "min_length") } + if self.sourceRateControl.utf8.count > 64 { throw ContractValidationError(field: "source_rate_control", code: "max_length") } + if self.clientDecode.isEmpty { throw ContractValidationError(field: "client_decode", code: "required") } + if !self.clientDecode.isEmpty && self.clientDecode.utf8.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_length") } + if self.clientDecode.utf8.count > 64 { throw ContractValidationError(field: "client_decode", code: "max_length") } + } + + 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) } } public struct ChannelFrame: Codable, Equatable { @@ -164,17 +310,44 @@ public struct ChannelFrame: Codable, Equatable { case payload = "payload" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - flowId = try c.decode(String.self, forKey: .flowId) - sequence = try c.decode(Int64.self, forKey: .sequence) - flags = try c.decode(Int64.self, forKey: .flags) - fragmentIndex = try c.decode(Int64.self, forKey: .fragmentIndex) - fragmentCount = try c.decode(Int64.self, forKey: .fragmentCount) - timestampMs = try c.decode(Int64.self, forKey: .timestampMs) - payload = try c.decode(String.self, forKey: .payload) + public init(version: String, flowId: String, sequence: Int64, flags: Int64, fragmentIndex: Int64, fragmentCount: Int64, timestampMs: Int64, payload: String) throws { + self.version = version + self.flowId = flowId + self.sequence = sequence + self.flags = flags + self.fragmentIndex = fragmentIndex + self.fragmentCount = fragmentCount + self.timestampMs = timestampMs + self.payload = payload + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), flowId: try c.decode(String.self, forKey: .flowId), sequence: try c.decode(Int64.self, forKey: .sequence), flags: try c.decode(Int64.self, forKey: .flags), fragmentIndex: try c.decode(Int64.self, forKey: .fragmentIndex), fragmentCount: try c.decode(Int64.self, forKey: .fragmentCount), timestampMs: try c.decode(Int64.self, forKey: .timestampMs), payload: try c.decode(String.self, forKey: .payload)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.flowId.isEmpty { throw ContractValidationError(field: "flow_id", code: "required") } + if !self.flowId.isEmpty && self.flowId.utf8.count < 1 { throw ContractValidationError(field: "flow_id", code: "min_length") } + if self.flowId.utf8.count > 64 { throw ContractValidationError(field: "flow_id", code: "max_length") } + if self.sequence < 0 { throw ContractValidationError(field: "sequence", code: "minimum") } + if self.flags < 0 { throw ContractValidationError(field: "flags", code: "minimum") } + if self.flags > 255 { throw ContractValidationError(field: "flags", code: "maximum") } + if self.fragmentIndex < 0 { throw ContractValidationError(field: "fragment_index", code: "minimum") } + if self.fragmentIndex > 15 { throw ContractValidationError(field: "fragment_index", code: "maximum") } + if self.fragmentCount < 1 { throw ContractValidationError(field: "fragment_count", code: "minimum") } + if self.fragmentCount > 16 { throw ContractValidationError(field: "fragment_count", code: "maximum") } + if self.timestampMs < 0 { throw ContractValidationError(field: "timestamp_ms", code: "minimum") } + if self.payload.utf8.count > 87384 { throw ContractValidationError(field: "payload", code: "max_length") } + if fragmentIndex >= fragmentCount { throw ContractValidationError(field: "fragment_index", code: "invalid_order") } + } + + 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) } } public struct ClipboardText: Codable, Equatable { @@ -185,11 +358,26 @@ public struct ClipboardText: Codable, Equatable { case encoding = "encoding" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - text = try c.decode(String.self, forKey: .text) - encoding = try c.decode(String.self, forKey: .encoding) + public init(text: String, encoding: String) throws { + self.text = text + self.encoding = encoding + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(text: try c.decode(String.self, forKey: .text), encoding: try c.decode(String.self, forKey: .encoding)) + } + + public func validate() throws { + if self.text.utf8.count > 65536 { throw ContractValidationError(field: "text", code: "max_length") } + if self.encoding != "utf-8" { throw ContractValidationError(field: "encoding", code: "invalid_value") } + } + + 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) } } public struct ConnectionManifest: Codable, Equatable { @@ -214,18 +402,44 @@ public struct ConnectionManifest: Codable, Equatable { case correlationId = "correlation_id" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - purpose = try c.decode(String.self, forKey: .purpose) - sessionId = try c.decode(String.self, forKey: .sessionId) - reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence) - gateway = try c.decode(ManifestGateway.self, forKey: .gateway) - tunnel = try c.decode(ManifestTunnel.self, forKey: .tunnel) - profile = try c.decode(ManifestProfile.self, forKey: .profile) - grant = try c.decode(GrantReference.self, forKey: .grant) - correlationId = try c.decode(String.self, forKey: .correlationId) + public init(version: String, purpose: String, sessionId: String, reconnectSequence: Int64, gateway: ManifestGateway, tunnel: ManifestTunnel, profile: ManifestProfile, grant: GrantReference, correlationId: String) throws { + self.version = version + self.purpose = purpose + self.sessionId = sessionId + self.reconnectSequence = reconnectSequence + self.gateway = gateway + self.tunnel = tunnel + self.profile = profile + self.grant = grant + self.correlationId = correlationId + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), purpose: try c.decode(String.self, forKey: .purpose), sessionId: try c.decode(String.self, forKey: .sessionId), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), gateway: try c.decode(ManifestGateway.self, forKey: .gateway), tunnel: try c.decode(ManifestTunnel.self, forKey: .tunnel), profile: try c.decode(ManifestProfile.self, forKey: .profile), grant: try c.decode(GrantReference.self, forKey: .grant), correlationId: try c.decode(String.self, forKey: .correlationId)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if !["launch", "reconnect"].contains(self.purpose) { throw ContractValidationError(field: "purpose", code: "invalid_value") } + if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") } + if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") } + if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") } + if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } + try self.gateway.validate() + try self.tunnel.validate() + try self.profile.validate() + try self.grant.validate() + if self.correlationId.isEmpty { throw ContractValidationError(field: "correlation_id", code: "required") } + if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") } + if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") } + } + + 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) } } public struct DeviceChallenge: Codable, Equatable { @@ -246,16 +460,45 @@ public struct DeviceChallenge: Codable, Equatable { case signatureFormat = "signature_format" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - deviceId = try c.decode(String.self, forKey: .deviceId) - serverId = try c.decode(String.self, forKey: .serverId) - principalId = try c.decode(String.self, forKey: .principalId) - challenge = try c.decode(String.self, forKey: .challenge) - expiresAt = try c.decode(String.self, forKey: .expiresAt) - algorithm = try c.decode(String.self, forKey: .algorithm) - signatureFormat = try c.decode(String.self, forKey: .signatureFormat) + public init(deviceId: String, serverId: String, principalId: String, challenge: String, expiresAt: String, algorithm: String, signatureFormat: String) throws { + self.deviceId = deviceId + self.serverId = serverId + self.principalId = principalId + self.challenge = challenge + self.expiresAt = expiresAt + self.algorithm = algorithm + self.signatureFormat = signatureFormat + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(deviceId: try c.decode(String.self, forKey: .deviceId), serverId: try c.decode(String.self, forKey: .serverId), principalId: try c.decode(String.self, forKey: .principalId), challenge: try c.decode(String.self, forKey: .challenge), expiresAt: try c.decode(String.self, forKey: .expiresAt), algorithm: try c.decode(String.self, forKey: .algorithm), signatureFormat: try c.decode(String.self, forKey: .signatureFormat)) + } + + public func validate() throws { + if self.deviceId.isEmpty { throw ContractValidationError(field: "device_id", code: "required") } + if !self.deviceId.isEmpty && self.deviceId.utf8.count < 1 { throw ContractValidationError(field: "device_id", code: "min_length") } + if self.deviceId.utf8.count > 128 { throw ContractValidationError(field: "device_id", code: "max_length") } + if self.serverId.isEmpty { throw ContractValidationError(field: "server_id", code: "required") } + if !self.serverId.isEmpty && self.serverId.utf8.count < 1 { throw ContractValidationError(field: "server_id", code: "min_length") } + if self.serverId.utf8.count > 128 { throw ContractValidationError(field: "server_id", code: "max_length") } + if self.principalId.isEmpty { throw ContractValidationError(field: "principal_id", code: "required") } + if !self.principalId.isEmpty && self.principalId.utf8.count < 1 { throw ContractValidationError(field: "principal_id", code: "min_length") } + if self.principalId.utf8.count > 128 { throw ContractValidationError(field: "principal_id", code: "max_length") } + if self.challenge.isEmpty { throw ContractValidationError(field: "challenge", code: "required") } + if !self.challenge.isEmpty && self.challenge.utf8.count < 1 { throw ContractValidationError(field: "challenge", code: "min_length") } + if self.challenge.utf8.count > 256 { throw ContractValidationError(field: "challenge", code: "max_length") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + if self.algorithm != "ed25519" { throw ContractValidationError(field: "algorithm", code: "invalid_value") } + if self.signatureFormat != "ed25519-domain-separated-v1" { throw ContractValidationError(field: "signature_format", code: "invalid_value") } + } + + 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) } } public struct DeviceProofRequest: Codable, Equatable { @@ -266,11 +509,30 @@ public struct DeviceProofRequest: Codable, Equatable { case signature = "signature" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - challenge = try c.decode(String.self, forKey: .challenge) - signature = try c.decode(String.self, forKey: .signature) + public init(challenge: String, signature: String) throws { + self.challenge = challenge + self.signature = signature + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(challenge: try c.decode(String.self, forKey: .challenge), signature: try c.decode(String.self, forKey: .signature)) + } + + public func validate() throws { + if self.challenge.isEmpty { throw ContractValidationError(field: "challenge", code: "required") } + if !self.challenge.isEmpty && self.challenge.utf8.count < 1 { throw ContractValidationError(field: "challenge", code: "min_length") } + if self.challenge.utf8.count > 256 { throw ContractValidationError(field: "challenge", code: "max_length") } + if self.signature.isEmpty { throw ContractValidationError(field: "signature", code: "required") } + if !self.signature.isEmpty && self.signature.utf8.count < 1 { throw ContractValidationError(field: "signature", code: "min_length") } + if self.signature.utf8.count > 256 { throw ContractValidationError(field: "signature", code: "max_length") } + } + + 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) } } public struct DeviceRegistrationRequest: Codable, Equatable { @@ -287,14 +549,40 @@ public struct DeviceRegistrationRequest: Codable, Equatable { case publicKey = "public_key" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - name = try c.decode(String.self, forKey: .name) - platform = try c.decode(String.self, forKey: .platform) - deviceSubject = try c.decode(String.self, forKey: .deviceSubject) - algorithm = try c.decode(String.self, forKey: .algorithm) - publicKey = try c.decode(String.self, forKey: .publicKey) + public init(name: String, platform: String, deviceSubject: String, algorithm: String, publicKey: String) throws { + self.name = name + self.platform = platform + self.deviceSubject = deviceSubject + self.algorithm = algorithm + self.publicKey = publicKey + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(name: try c.decode(String.self, forKey: .name), platform: try c.decode(String.self, forKey: .platform), deviceSubject: try c.decode(String.self, forKey: .deviceSubject), algorithm: try c.decode(String.self, forKey: .algorithm), publicKey: try c.decode(String.self, forKey: .publicKey)) + } + + public func validate() throws { + if self.name.isEmpty { throw ContractValidationError(field: "name", code: "required") } + if !self.name.isEmpty && self.name.utf8.count < 1 { throw ContractValidationError(field: "name", code: "min_length") } + if self.name.utf8.count > 128 { throw ContractValidationError(field: "name", code: "max_length") } + if self.platform.isEmpty { throw ContractValidationError(field: "platform", code: "required") } + if !self.platform.isEmpty && self.platform.utf8.count < 1 { throw ContractValidationError(field: "platform", code: "min_length") } + if self.platform.utf8.count > 64 { throw ContractValidationError(field: "platform", code: "max_length") } + if self.deviceSubject.isEmpty { throw ContractValidationError(field: "device_subject", code: "required") } + if !self.deviceSubject.isEmpty && self.deviceSubject.utf8.count < 1 { throw ContractValidationError(field: "device_subject", code: "min_length") } + if self.deviceSubject.utf8.count > 256 { throw ContractValidationError(field: "device_subject", code: "max_length") } + if self.algorithm != "ed25519" { throw ContractValidationError(field: "algorithm", code: "invalid_value") } + if self.publicKey.isEmpty { throw ContractValidationError(field: "public_key", code: "required") } + if !self.publicKey.isEmpty && self.publicKey.utf8.count < 1 { throw ContractValidationError(field: "public_key", code: "min_length") } + if self.publicKey.utf8.count > 256 { throw ContractValidationError(field: "public_key", code: "max_length") } + } + + 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) } } public struct EntitledPool: Codable, Equatable { @@ -307,12 +595,34 @@ public struct EntitledPool: Codable, Equatable { case assignmentState = "assignment_state" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - poolId = try c.decode(String.self, forKey: .poolId) - name = try c.decode(String.self, forKey: .name) - assignmentState = try c.decode(String.self, forKey: .assignmentState) + public init(poolId: String, name: String, assignmentState: String) throws { + self.poolId = poolId + self.name = name + self.assignmentState = assignmentState + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(poolId: try c.decode(String.self, forKey: .poolId), name: try c.decode(String.self, forKey: .name), assignmentState: try c.decode(String.self, forKey: .assignmentState)) + } + + public func validate() throws { + if self.poolId.isEmpty { throw ContractValidationError(field: "pool_id", code: "required") } + if !self.poolId.isEmpty && self.poolId.utf8.count < 1 { throw ContractValidationError(field: "pool_id", code: "min_length") } + if self.poolId.utf8.count > 128 { throw ContractValidationError(field: "pool_id", code: "max_length") } + if self.name.isEmpty { throw ContractValidationError(field: "name", code: "required") } + if !self.name.isEmpty && self.name.utf8.count < 1 { throw ContractValidationError(field: "name", code: "min_length") } + if self.name.utf8.count > 256 { throw ContractValidationError(field: "name", code: "max_length") } + if self.assignmentState.isEmpty { throw ContractValidationError(field: "assignment_state", code: "required") } + if !self.assignmentState.isEmpty && self.assignmentState.utf8.count < 1 { throw ContractValidationError(field: "assignment_state", code: "min_length") } + if self.assignmentState.utf8.count > 64 { throw ContractValidationError(field: "assignment_state", code: "max_length") } + } + + 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) } } public struct ErrorEnvelope: Codable, Equatable { @@ -333,16 +643,46 @@ public struct ErrorEnvelope: Codable, Equatable { case violations = "violations" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - status = try c.decode(Bool.self, forKey: .status) - error = try c.decode(String.self, forKey: .error) - code = try c.decode(String.self, forKey: .code) - message = try c.decode(String.self, forKey: .message) - resolution = try c.decode(String.self, forKey: .resolution) - requestId = try c.decode(String.self, forKey: .requestId) - violations = try c.decode([FieldViolation].self, forKey: .violations) + public init(status: Bool, error: String, code: String, message: String, resolution: String, requestId: String, violations: [FieldViolation]) throws { + self.status = status + self.error = error + self.code = code + self.message = message + self.resolution = resolution + self.requestId = requestId + self.violations = violations + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(status: try c.decode(Bool.self, forKey: .status), error: try c.decode(String.self, forKey: .error), code: try c.decode(String.self, forKey: .code), message: try c.decode(String.self, forKey: .message), resolution: try c.decode(String.self, forKey: .resolution), requestId: try c.decode(String.self, forKey: .requestId), violations: try c.decode([FieldViolation].self, forKey: .violations)) + } + + public func validate() throws { + if self.error.isEmpty { throw ContractValidationError(field: "error", code: "required") } + if !self.error.isEmpty && self.error.utf8.count < 1 { throw ContractValidationError(field: "error", code: "min_length") } + if self.error.utf8.count > 512 { throw ContractValidationError(field: "error", code: "max_length") } + if self.code.isEmpty { throw ContractValidationError(field: "code", code: "required") } + if !self.code.isEmpty && self.code.utf8.count < 1 { throw ContractValidationError(field: "code", code: "min_length") } + if self.code.utf8.count > 128 { throw ContractValidationError(field: "code", code: "max_length") } + if self.message.isEmpty { throw ContractValidationError(field: "message", code: "required") } + if !self.message.isEmpty && self.message.utf8.count < 1 { throw ContractValidationError(field: "message", code: "min_length") } + if self.message.utf8.count > 512 { throw ContractValidationError(field: "message", code: "max_length") } + if self.resolution.isEmpty { throw ContractValidationError(field: "resolution", code: "required") } + if !self.resolution.isEmpty && self.resolution.utf8.count < 1 { throw ContractValidationError(field: "resolution", code: "min_length") } + if self.resolution.utf8.count > 128 { throw ContractValidationError(field: "resolution", code: "max_length") } + if self.requestId.isEmpty { throw ContractValidationError(field: "request_id", code: "required") } + if !self.requestId.isEmpty && self.requestId.utf8.count < 1 { throw ContractValidationError(field: "request_id", code: "min_length") } + if self.requestId.utf8.count > 128 { throw ContractValidationError(field: "request_id", code: "max_length") } + if self.violations.count > 16 { throw ContractValidationError(field: "violations", code: "max_items") } + for item in self.violations { try item.validate() } + } + + 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) } } public struct EventEnvelope: Codable, Equatable { @@ -365,17 +705,44 @@ public struct EventEnvelope: Codable, Equatable { case payload = "payload" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - eventId = try c.decode(String.self, forKey: .eventId) - sequence = try c.decode(Int64.self, forKey: .sequence) - type = try c.decode(String.self, forKey: .type) - version = try c.decode(Int64.self, forKey: .version) - resource = try c.decode(ResourceLink.self, forKey: .resource) - occurredAt = try c.decode(String.self, forKey: .occurredAt) - correlationId = try c.decode(String.self, forKey: .correlationId) - payload = try c.decode(JSONObject.self, forKey: .payload) + public init(eventId: String, sequence: Int64, type: String, version: Int64, resource: ResourceLink, occurredAt: String, correlationId: String, payload: JSONObject) throws { + self.eventId = eventId + self.sequence = sequence + self.type = type + self.version = version + self.resource = resource + self.occurredAt = occurredAt + self.correlationId = correlationId + self.payload = payload + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(eventId: try c.decode(String.self, forKey: .eventId), sequence: try c.decode(Int64.self, forKey: .sequence), type: try c.decode(String.self, forKey: .type), version: try c.decode(Int64.self, forKey: .version), resource: try c.decode(ResourceLink.self, forKey: .resource), occurredAt: try c.decode(String.self, forKey: .occurredAt), correlationId: try c.decode(String.self, forKey: .correlationId), payload: try c.decode(JSONObject.self, forKey: .payload)) + } + + public func validate() throws { + if self.eventId.isEmpty { throw ContractValidationError(field: "event_id", code: "required") } + if !self.eventId.isEmpty && self.eventId.utf8.count < 1 { throw ContractValidationError(field: "event_id", code: "min_length") } + if self.eventId.utf8.count > 128 { throw ContractValidationError(field: "event_id", code: "max_length") } + if self.sequence < 1 { throw ContractValidationError(field: "sequence", code: "minimum") } + if self.type.isEmpty { throw ContractValidationError(field: "type", code: "required") } + if !self.type.isEmpty && self.type.utf8.count < 1 { throw ContractValidationError(field: "type", code: "min_length") } + if self.type.utf8.count > 128 { throw ContractValidationError(field: "type", code: "max_length") } + if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } + try self.resource.validate() + if self.occurredAt.utf8.count > 64 { throw ContractValidationError(field: "occurred_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.occurredAt) == nil { throw ContractValidationError(field: "occurred_at", code: "invalid_time") } + if self.correlationId.isEmpty { throw ContractValidationError(field: "correlation_id", code: "required") } + if !self.correlationId.isEmpty && self.correlationId.utf8.count < 1 { throw ContractValidationError(field: "correlation_id", code: "min_length") } + if self.correlationId.utf8.count > 128 { throw ContractValidationError(field: "correlation_id", code: "max_length") } + } + + 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) } } public struct EventResume: Codable, Equatable { @@ -386,11 +753,26 @@ public struct EventResume: Codable, Equatable { case lastSequence = "last_sequence" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - cursor = try c.decode(String.self, forKey: .cursor) - lastSequence = try c.decode(Int64.self, forKey: .lastSequence) + public init(cursor: String, lastSequence: Int64) throws { + self.cursor = cursor + self.lastSequence = lastSequence + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(cursor: try c.decode(String.self, forKey: .cursor), lastSequence: try c.decode(Int64.self, forKey: .lastSequence)) + } + + public func validate() throws { + if self.cursor.utf8.count > 512 { throw ContractValidationError(field: "cursor", code: "max_length") } + if self.lastSequence < 0 { throw ContractValidationError(field: "last_sequence", code: "minimum") } + } + + 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) } } public struct FieldViolation: Codable, Equatable { @@ -401,11 +783,30 @@ public struct FieldViolation: Codable, Equatable { case code = "code" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - field = try c.decode(String.self, forKey: .field) - code = try c.decode(String.self, forKey: .code) + public init(field: String, code: String) throws { + self.field = field + self.code = code + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(field: try c.decode(String.self, forKey: .field), code: try c.decode(String.self, forKey: .code)) + } + + public func validate() throws { + if self.field.isEmpty { throw ContractValidationError(field: "field", code: "required") } + if !self.field.isEmpty && self.field.utf8.count < 1 { throw ContractValidationError(field: "field", code: "min_length") } + if self.field.utf8.count > 128 { throw ContractValidationError(field: "field", code: "max_length") } + if self.code.isEmpty { throw ContractValidationError(field: "code", code: "required") } + if !self.code.isEmpty && self.code.utf8.count < 1 { throw ContractValidationError(field: "code", code: "min_length") } + if self.code.utf8.count > 64 { throw ContractValidationError(field: "code", code: "max_length") } + } + + 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) } } public struct GatewayDrain: Codable, Equatable { @@ -422,14 +823,37 @@ public struct GatewayDrain: Codable, Equatable { case deadline = "deadline" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - gatewayId = try c.decode(String.self, forKey: .gatewayId) - sequence = try c.decode(Int64.self, forKey: .sequence) - reason = try c.decode(String.self, forKey: .reason) - deadline = try c.decode(String.self, forKey: .deadline) + public init(version: String, gatewayId: String, sequence: Int64, reason: String, deadline: String) throws { + self.version = version + self.gatewayId = gatewayId + self.sequence = sequence + self.reason = reason + self.deadline = deadline + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), gatewayId: try c.decode(String.self, forKey: .gatewayId), sequence: try c.decode(Int64.self, forKey: .sequence), reason: try c.decode(String.self, forKey: .reason), deadline: try c.decode(String.self, forKey: .deadline)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") } + if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") } + if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } + if self.sequence < 1 { throw ContractValidationError(field: "sequence", code: "minimum") } + if self.reason.isEmpty { throw ContractValidationError(field: "reason", code: "required") } + if !self.reason.isEmpty && self.reason.utf8.count < 1 { throw ContractValidationError(field: "reason", code: "min_length") } + if self.reason.utf8.count > 256 { throw ContractValidationError(field: "reason", code: "max_length") } + if self.deadline.utf8.count > 64 { throw ContractValidationError(field: "deadline", code: "max_length") } + if ISO8601DateFormatter().date(from: self.deadline) == nil { throw ContractValidationError(field: "deadline", code: "invalid_time") } + } + + 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) } } public struct GatewayHeartbeat: Codable, Equatable { @@ -450,16 +874,41 @@ public struct GatewayHeartbeat: Codable, Equatable { case state = "state" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - gatewayId = try c.decode(String.self, forKey: .gatewayId) - sequence = try c.decode(Int64.self, forKey: .sequence) - observedAt = try c.decode(String.self, forKey: .observedAt) - activeConnections = try c.decode(Int64.self, forKey: .activeConnections) - egressKbps = try c.decode(Int64.self, forKey: .egressKbps) - state = try c.decode(String.self, forKey: .state) + public init(version: String, gatewayId: String, sequence: Int64, observedAt: String, activeConnections: Int64, egressKbps: Int64, state: String) throws { + self.version = version + self.gatewayId = gatewayId + self.sequence = sequence + self.observedAt = observedAt + self.activeConnections = activeConnections + self.egressKbps = egressKbps + self.state = state + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), gatewayId: try c.decode(String.self, forKey: .gatewayId), sequence: try c.decode(Int64.self, forKey: .sequence), observedAt: try c.decode(String.self, forKey: .observedAt), activeConnections: try c.decode(Int64.self, forKey: .activeConnections), egressKbps: try c.decode(Int64.self, forKey: .egressKbps), state: try c.decode(String.self, forKey: .state)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") } + if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") } + if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } + if self.sequence < 1 { throw ContractValidationError(field: "sequence", code: "minimum") } + if self.observedAt.utf8.count > 64 { throw ContractValidationError(field: "observed_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.observedAt) == nil { throw ContractValidationError(field: "observed_at", code: "invalid_time") } + if self.activeConnections < 0 { throw ContractValidationError(field: "active_connections", code: "minimum") } + if self.activeConnections > 1000000 { throw ContractValidationError(field: "active_connections", code: "maximum") } + if self.egressKbps < 0 { throw ContractValidationError(field: "egress_kbps", code: "minimum") } + if self.egressKbps > 1000000000 { throw ContractValidationError(field: "egress_kbps", code: "maximum") } + if !["ready", "draining", "offline"].contains(self.state) { throw ContractValidationError(field: "state", code: "invalid_value") } + } + + 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) } } public struct GatewayRegistration: Codable, Equatable { @@ -492,22 +941,65 @@ public struct GatewayRegistration: Codable, Equatable { case capabilities = "capabilities" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - gatewayId = try c.decode(String.self, forKey: .gatewayId) - instanceIdentity = try c.decode(String.self, forKey: .instanceIdentity) - certificateIdentity = try c.decode(String.self, forKey: .certificateIdentity) - publicIdentity = try c.decode(String.self, forKey: .publicIdentity) - address = try c.decode(String.self, forKey: .address) - providerIdentity = try c.decode(String.self, forKey: .providerIdentity) - protocolMinVersion = try c.decode(Int64.self, forKey: .protocolMinVersion) - protocolMaxVersion = try c.decode(Int64.self, forKey: .protocolMaxVersion) - connectionCapacity = try c.decode(Int64.self, forKey: .connectionCapacity) - bandwidthCapacityKbps = try c.decode(Int64.self, forKey: .bandwidthCapacityKbps) - features = try c.decode([String].self, forKey: .features) - capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities) + public init(version: String, gatewayId: String, instanceIdentity: String, certificateIdentity: String, publicIdentity: String, address: String, providerIdentity: String, protocolMinVersion: Int64, protocolMaxVersion: Int64, connectionCapacity: Int64, bandwidthCapacityKbps: Int64, features: [String], capabilities: CapabilityProfile) throws { + self.version = version + self.gatewayId = gatewayId + self.instanceIdentity = instanceIdentity + self.certificateIdentity = certificateIdentity + self.publicIdentity = publicIdentity + self.address = address + self.providerIdentity = providerIdentity + self.protocolMinVersion = protocolMinVersion + self.protocolMaxVersion = protocolMaxVersion + self.connectionCapacity = connectionCapacity + self.bandwidthCapacityKbps = bandwidthCapacityKbps + self.features = features + self.capabilities = capabilities + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), gatewayId: try c.decode(String.self, forKey: .gatewayId), instanceIdentity: try c.decode(String.self, forKey: .instanceIdentity), certificateIdentity: try c.decode(String.self, forKey: .certificateIdentity), publicIdentity: try c.decode(String.self, forKey: .publicIdentity), address: try c.decode(String.self, forKey: .address), providerIdentity: try c.decode(String.self, forKey: .providerIdentity), protocolMinVersion: try c.decode(Int64.self, forKey: .protocolMinVersion), protocolMaxVersion: try c.decode(Int64.self, forKey: .protocolMaxVersion), connectionCapacity: try c.decode(Int64.self, forKey: .connectionCapacity), bandwidthCapacityKbps: try c.decode(Int64.self, forKey: .bandwidthCapacityKbps), features: try c.decode([String].self, forKey: .features), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") } + if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") } + if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } + if self.instanceIdentity.isEmpty { throw ContractValidationError(field: "instance_identity", code: "required") } + if !self.instanceIdentity.isEmpty && self.instanceIdentity.utf8.count < 1 { throw ContractValidationError(field: "instance_identity", code: "min_length") } + if self.instanceIdentity.utf8.count > 512 { throw ContractValidationError(field: "instance_identity", code: "max_length") } + if self.certificateIdentity.isEmpty { throw ContractValidationError(field: "certificate_identity", code: "required") } + if !self.certificateIdentity.isEmpty && self.certificateIdentity.utf8.count < 1 { throw ContractValidationError(field: "certificate_identity", code: "min_length") } + if self.certificateIdentity.utf8.count > 512 { throw ContractValidationError(field: "certificate_identity", code: "max_length") } + if self.publicIdentity.isEmpty { throw ContractValidationError(field: "public_identity", code: "required") } + if !self.publicIdentity.isEmpty && self.publicIdentity.utf8.count < 1 { throw ContractValidationError(field: "public_identity", code: "min_length") } + if self.publicIdentity.utf8.count > 256 { throw ContractValidationError(field: "public_identity", code: "max_length") } + if self.address.isEmpty { throw ContractValidationError(field: "address", code: "required") } + if !self.address.isEmpty && self.address.utf8.count < 1 { throw ContractValidationError(field: "address", code: "min_length") } + if self.address.utf8.count > 256 { throw ContractValidationError(field: "address", code: "max_length") } + if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") } + if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") } + if self.providerIdentity.utf8.count > 256 { throw ContractValidationError(field: "provider_identity", code: "max_length") } + if self.protocolMinVersion < 1 { throw ContractValidationError(field: "protocol_min_version", code: "minimum") } + if self.protocolMinVersion > 100 { throw ContractValidationError(field: "protocol_min_version", code: "maximum") } + if self.protocolMaxVersion < 1 { throw ContractValidationError(field: "protocol_max_version", code: "minimum") } + if self.protocolMaxVersion > 100 { throw ContractValidationError(field: "protocol_max_version", code: "maximum") } + if self.connectionCapacity < 1 { throw ContractValidationError(field: "connection_capacity", code: "minimum") } + if self.connectionCapacity > 1000000 { throw ContractValidationError(field: "connection_capacity", code: "maximum") } + if self.bandwidthCapacityKbps < 1 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "minimum") } + if self.bandwidthCapacityKbps > 1000000000 { throw ContractValidationError(field: "bandwidth_capacity_kbps", code: "maximum") } + if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") } + try self.capabilities.validate() + if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: "protocol_version", code: "invalid_order") } + } + + 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) } } public struct GrantReference: Codable, Equatable { @@ -520,12 +1012,33 @@ public struct GrantReference: Codable, Equatable { case audience = "audience" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - opaqueValue = try c.decode(String.self, forKey: .opaqueValue) - expiresAt = try c.decode(String.self, forKey: .expiresAt) - audience = try c.decode(String.self, forKey: .audience) + public init(opaqueValue: String, expiresAt: String, audience: String) throws { + self.opaqueValue = opaqueValue + self.expiresAt = expiresAt + self.audience = audience + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(opaqueValue: try c.decode(String.self, forKey: .opaqueValue), expiresAt: try c.decode(String.self, forKey: .expiresAt), audience: try c.decode(String.self, forKey: .audience)) + } + + public func validate() throws { + if self.opaqueValue.isEmpty { throw ContractValidationError(field: "opaque_value", code: "required") } + if !self.opaqueValue.isEmpty && self.opaqueValue.utf8.count < 43 { throw ContractValidationError(field: "opaque_value", code: "min_length") } + if self.opaqueValue.utf8.count > 256 { throw ContractValidationError(field: "opaque_value", code: "max_length") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") } + if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") } + if self.audience.utf8.count > 128 { throw ContractValidationError(field: "audience", code: "max_length") } + } + + 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) } } public struct LoginRequest: Codable, Equatable { @@ -538,12 +1051,34 @@ public struct LoginRequest: Codable, Equatable { case password = "password" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - provider = try c.decodeIfPresent(String.self, forKey: .provider) - username = try c.decode(String.self, forKey: .username) - password = try c.decode(String.self, forKey: .password) + public init(provider: String?, username: String, password: String) throws { + self.provider = provider + self.username = username + self.password = password + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(provider: try c.decodeIfPresent(String.self, forKey: .provider), username: try c.decode(String.self, forKey: .username), password: try c.decode(String.self, forKey: .password)) + } + + public func validate() throws { + if let value = self.provider { + if !["ldap", "local"].contains(value) { throw ContractValidationError(field: "provider", code: "invalid_value") } + } + if self.username.isEmpty { throw ContractValidationError(field: "username", code: "required") } + if !self.username.isEmpty && self.username.utf8.count < 1 { throw ContractValidationError(field: "username", code: "min_length") } + if self.username.utf8.count > 256 { throw ContractValidationError(field: "username", code: "max_length") } + if self.password.isEmpty { throw ContractValidationError(field: "password", code: "required") } + if !self.password.isEmpty && self.password.utf8.count < 1 { throw ContractValidationError(field: "password", code: "min_length") } + if self.password.utf8.count > 1024 { throw ContractValidationError(field: "password", code: "max_length") } + } + + 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) } } public struct ManifestBounds: Codable, Equatable { @@ -556,12 +1091,32 @@ public struct ManifestBounds: Codable, Equatable { case maximumKbps = "maximum_kbps" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps) - targetKbps = try c.decode(Int64.self, forKey: .targetKbps) - maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps) + public init(minimumKbps: Int64, targetKbps: Int64, maximumKbps: Int64) throws { + self.minimumKbps = minimumKbps + self.targetKbps = targetKbps + self.maximumKbps = maximumKbps + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(minimumKbps: try c.decode(Int64.self, forKey: .minimumKbps), targetKbps: try c.decode(Int64.self, forKey: .targetKbps), maximumKbps: try c.decode(Int64.self, forKey: .maximumKbps)) + } + + public func validate() throws { + if self.minimumKbps < 1 { throw ContractValidationError(field: "minimum_kbps", code: "minimum") } + if self.minimumKbps > 100000000 { throw ContractValidationError(field: "minimum_kbps", code: "maximum") } + if self.targetKbps < 1 { throw ContractValidationError(field: "target_kbps", code: "minimum") } + if self.targetKbps > 100000000 { throw ContractValidationError(field: "target_kbps", code: "maximum") } + if self.maximumKbps < 1 { throw ContractValidationError(field: "maximum_kbps", code: "minimum") } + if self.maximumKbps > 100000000 { throw ContractValidationError(field: "maximum_kbps", code: "maximum") } + if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: "bounds", code: "invalid_order") } + } + + 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) } } public struct ManifestGateway: Codable, Equatable { @@ -574,12 +1129,33 @@ public struct ManifestGateway: Codable, Equatable { case publicIdentity = "public_identity" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - id = try c.decode(String.self, forKey: .id) - addresses = try c.decode([String].self, forKey: .addresses) - publicIdentity = try c.decode(String.self, forKey: .publicIdentity) + public init(id: String, addresses: [String], publicIdentity: String) throws { + self.id = id + self.addresses = addresses + self.publicIdentity = publicIdentity + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(id: try c.decode(String.self, forKey: .id), addresses: try c.decode([String].self, forKey: .addresses), publicIdentity: try c.decode(String.self, forKey: .publicIdentity)) + } + + public func validate() throws { + if self.id.isEmpty { throw ContractValidationError(field: "id", code: "required") } + if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } + if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } + if self.addresses.count < 1 { throw ContractValidationError(field: "addresses", code: "min_items") } + if self.addresses.count > 4 { throw ContractValidationError(field: "addresses", code: "max_items") } + if self.publicIdentity.isEmpty { throw ContractValidationError(field: "public_identity", code: "required") } + if !self.publicIdentity.isEmpty && self.publicIdentity.utf8.count < 1 { throw ContractValidationError(field: "public_identity", code: "min_length") } + if self.publicIdentity.utf8.count > 256 { throw ContractValidationError(field: "public_identity", code: "max_length") } + } + + 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) } } public struct ManifestProfile: Codable, Equatable { @@ -590,11 +1166,28 @@ public struct ManifestProfile: Codable, Equatable { case bounds = "bounds" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - id = try c.decode(String.self, forKey: .id) - bounds = try c.decode(ManifestBounds.self, forKey: .bounds) + public init(id: String, bounds: ManifestBounds) throws { + self.id = id + self.bounds = bounds + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(id: try c.decode(String.self, forKey: .id), bounds: try c.decode(ManifestBounds.self, forKey: .bounds)) + } + + public func validate() throws { + if self.id.isEmpty { throw ContractValidationError(field: "id", code: "required") } + if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } + if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } + try self.bounds.validate() + } + + 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) } } public struct ManifestTunnel: Codable, Equatable { @@ -605,11 +1198,27 @@ public struct ManifestTunnel: Codable, Equatable { case features = "features" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - versions = try c.decode([String].self, forKey: .versions) - features = try c.decode([String].self, forKey: .features) + public init(versions: [String], features: [String]) throws { + self.versions = versions + self.features = features + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(versions: try c.decode([String].self, forKey: .versions), features: try c.decode([String].self, forKey: .features)) + } + + public func validate() throws { + if self.versions.count < 1 { throw ContractValidationError(field: "versions", code: "min_items") } + if self.versions.count > 4 { throw ContractValidationError(field: "versions", code: "max_items") } + if self.features.count > 32 { throw ContractValidationError(field: "features", code: "max_items") } + } + + 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) } } public struct NativeCredential: Codable, Equatable { @@ -628,15 +1237,46 @@ public struct NativeCredential: Codable, Equatable { case refreshExpiresAt = "refresh_expires_at" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - deviceId = try c.decodeIfPresent(String.self, forKey: .deviceId) - familyId = try c.decode(String.self, forKey: .familyId) - accessToken = try c.decode(String.self, forKey: .accessToken) - refreshToken = try c.decode(String.self, forKey: .refreshToken) - expiresAt = try c.decode(String.self, forKey: .expiresAt) - refreshExpiresAt = try c.decodeIfPresent(String.self, forKey: .refreshExpiresAt) + public init(deviceId: String?, familyId: String, accessToken: String, refreshToken: String, expiresAt: String, refreshExpiresAt: String?) throws { + self.deviceId = deviceId + self.familyId = familyId + self.accessToken = accessToken + self.refreshToken = refreshToken + self.expiresAt = expiresAt + self.refreshExpiresAt = refreshExpiresAt + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(deviceId: try c.decodeIfPresent(String.self, forKey: .deviceId), familyId: try c.decode(String.self, forKey: .familyId), accessToken: try c.decode(String.self, forKey: .accessToken), refreshToken: try c.decode(String.self, forKey: .refreshToken), expiresAt: try c.decode(String.self, forKey: .expiresAt), refreshExpiresAt: try c.decodeIfPresent(String.self, forKey: .refreshExpiresAt)) + } + + public func validate() throws { + if let value = self.deviceId { + if value.utf8.count > 128 { throw ContractValidationError(field: "device_id", code: "max_length") } + } + if self.familyId.isEmpty { throw ContractValidationError(field: "family_id", code: "required") } + if !self.familyId.isEmpty && self.familyId.utf8.count < 1 { throw ContractValidationError(field: "family_id", code: "min_length") } + if self.familyId.utf8.count > 128 { throw ContractValidationError(field: "family_id", code: "max_length") } + if self.accessToken.isEmpty { throw ContractValidationError(field: "access_token", code: "required") } + if !self.accessToken.isEmpty && self.accessToken.utf8.count < 1 { throw ContractValidationError(field: "access_token", code: "min_length") } + if self.accessToken.utf8.count > 256 { throw ContractValidationError(field: "access_token", code: "max_length") } + if self.refreshToken.isEmpty { throw ContractValidationError(field: "refresh_token", code: "required") } + if !self.refreshToken.isEmpty && self.refreshToken.utf8.count < 1 { throw ContractValidationError(field: "refresh_token", code: "min_length") } + if self.refreshToken.utf8.count > 256 { throw ContractValidationError(field: "refresh_token", code: "max_length") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + if let value = self.refreshExpiresAt { + if value.utf8.count > 64 { throw ContractValidationError(field: "refresh_expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "refresh_expires_at", code: "invalid_time") } + } + } + + 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) } } public struct PageInfo: Codable, Equatable { @@ -647,11 +1287,27 @@ public struct PageInfo: Codable, Equatable { case nextCursor = "next_cursor" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - limit = try c.decode(Int64.self, forKey: .limit) - nextCursor = try c.decode(String.self, forKey: .nextCursor) + public init(limit: Int64, nextCursor: String) throws { + self.limit = limit + self.nextCursor = nextCursor + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(limit: try c.decode(Int64.self, forKey: .limit), nextCursor: try c.decode(String.self, forKey: .nextCursor)) + } + + public func validate() throws { + if self.limit < 1 { throw ContractValidationError(field: "limit", code: "minimum") } + if self.limit > 100 { throw ContractValidationError(field: "limit", code: "maximum") } + if self.nextCursor.utf8.count > 512 { throw ContractValidationError(field: "next_cursor", code: "max_length") } + } + + 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) } } public struct ProviderState: Codable, Equatable { @@ -668,14 +1324,33 @@ public struct ProviderState: Codable, Equatable { case channels = "channels" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - sessionId = try c.decode(String.self, forKey: .sessionId) - state = try c.decode(String.self, forKey: .state) - cleanupPending = try c.decode(Bool.self, forKey: .cleanupPending) - channels = try c.decode([String].self, forKey: .channels) + public init(version: String, sessionId: String, state: String, cleanupPending: Bool, channels: [String]) throws { + self.version = version + self.sessionId = sessionId + self.state = state + self.cleanupPending = cleanupPending + self.channels = channels + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), state: try c.decode(String.self, forKey: .state), cleanupPending: try c.decode(Bool.self, forKey: .cleanupPending), channels: try c.decode([String].self, forKey: .channels)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") } + if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") } + if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") } + if !["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"].contains(self.state) { throw ContractValidationError(field: "state", code: "invalid_value") } + if self.channels.count > 8 { throw ContractValidationError(field: "channels", code: "max_items") } + } + + 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) } } public struct ReauthGrant: Codable, Equatable { @@ -688,12 +1363,33 @@ public struct ReauthGrant: Codable, Equatable { case expiresAt = "expires_at" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - token = try c.decode(String.self, forKey: .token) - purpose = try c.decode(String.self, forKey: .purpose) - expiresAt = try c.decode(String.self, forKey: .expiresAt) + public init(token: String, purpose: String, expiresAt: String) throws { + self.token = token + self.purpose = purpose + self.expiresAt = expiresAt + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(token: try c.decode(String.self, forKey: .token), purpose: try c.decode(String.self, forKey: .purpose), expiresAt: try c.decode(String.self, forKey: .expiresAt)) + } + + public func validate() throws { + if self.token.isEmpty { throw ContractValidationError(field: "token", code: "required") } + if !self.token.isEmpty && self.token.utf8.count < 1 { throw ContractValidationError(field: "token", code: "min_length") } + if self.token.utf8.count > 256 { throw ContractValidationError(field: "token", code: "max_length") } + if self.purpose.isEmpty { throw ContractValidationError(field: "purpose", code: "required") } + if !self.purpose.isEmpty && self.purpose.utf8.count < 1 { throw ContractValidationError(field: "purpose", code: "min_length") } + if self.purpose.utf8.count > 64 { throw ContractValidationError(field: "purpose", code: "max_length") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + } + + 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) } } public struct ReauthRequest: Codable, Equatable { @@ -704,11 +1400,28 @@ public struct ReauthRequest: Codable, Equatable { case purpose = "purpose" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - password = try c.decode(String.self, forKey: .password) - purpose = try c.decode(String.self, forKey: .purpose) + public init(password: String, purpose: String) throws { + self.password = password + self.purpose = purpose + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(password: try c.decode(String.self, forKey: .password), purpose: try c.decode(String.self, forKey: .purpose)) + } + + public func validate() throws { + if self.password.isEmpty { throw ContractValidationError(field: "password", code: "required") } + if !self.password.isEmpty && self.password.utf8.count < 1 { throw ContractValidationError(field: "password", code: "min_length") } + if self.password.utf8.count > 1024 { throw ContractValidationError(field: "password", code: "max_length") } + if !["identity_change", "key_change", "backup_enable", "external_database_tls_disabled", "assignment_change"].contains(self.purpose) { throw ContractValidationError(field: "purpose", code: "invalid_value") } + } + + 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) } } public struct ReconnectRequest: Codable, Equatable { @@ -721,12 +1434,32 @@ public struct ReconnectRequest: Codable, Equatable { case expectedVersion = "expected_version" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId) - deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId) - expectedVersion = try c.decode(Int64.self, forKey: .expectedVersion) + public init(clientDeviceId: String, deviceKeyId: String, expectedVersion: Int64) throws { + self.clientDeviceId = clientDeviceId + self.deviceKeyId = deviceKeyId + self.expectedVersion = expectedVersion + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), expectedVersion: try c.decode(Int64.self, forKey: .expectedVersion)) + } + + public func validate() throws { + if self.clientDeviceId.isEmpty { throw ContractValidationError(field: "client_device_id", code: "required") } + if !self.clientDeviceId.isEmpty && self.clientDeviceId.utf8.count < 1 { throw ContractValidationError(field: "client_device_id", code: "min_length") } + if self.clientDeviceId.utf8.count > 128 { throw ContractValidationError(field: "client_device_id", code: "max_length") } + if self.deviceKeyId.isEmpty { throw ContractValidationError(field: "device_key_id", code: "required") } + if !self.deviceKeyId.isEmpty && self.deviceKeyId.utf8.count < 1 { throw ContractValidationError(field: "device_key_id", code: "min_length") } + if self.deviceKeyId.utf8.count > 128 { throw ContractValidationError(field: "device_key_id", code: "max_length") } + if self.expectedVersion < 1 { throw ContractValidationError(field: "expected_version", code: "minimum") } + } + + 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) } } public struct RefreshRequest: Codable, Equatable { @@ -737,11 +1470,30 @@ public struct RefreshRequest: Codable, Equatable { case refreshToken = "refresh_token" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - familyId = try c.decode(String.self, forKey: .familyId) - refreshToken = try c.decode(String.self, forKey: .refreshToken) + public init(familyId: String, refreshToken: String) throws { + self.familyId = familyId + self.refreshToken = refreshToken + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(familyId: try c.decode(String.self, forKey: .familyId), refreshToken: try c.decode(String.self, forKey: .refreshToken)) + } + + public func validate() throws { + if self.familyId.isEmpty { throw ContractValidationError(field: "family_id", code: "required") } + if !self.familyId.isEmpty && self.familyId.utf8.count < 1 { throw ContractValidationError(field: "family_id", code: "min_length") } + if self.familyId.utf8.count > 128 { throw ContractValidationError(field: "family_id", code: "max_length") } + if self.refreshToken.isEmpty { throw ContractValidationError(field: "refresh_token", code: "required") } + if !self.refreshToken.isEmpty && self.refreshToken.utf8.count < 1 { throw ContractValidationError(field: "refresh_token", code: "min_length") } + if self.refreshToken.utf8.count > 256 { throw ContractValidationError(field: "refresh_token", code: "max_length") } + } + + 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) } } public struct Resource: Codable, Equatable { @@ -762,16 +1514,47 @@ public struct Resource: Codable, Equatable { case links = "links" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - id = try c.decode(String.self, forKey: .id) - kind = try c.decode(String.self, forKey: .kind) - name = try c.decode(String.self, forKey: .name) - state = try c.decode(String.self, forKey: .state) - assignmentState = try c.decodeIfPresent(String.self, forKey: .assignmentState) - version = try c.decode(Int64.self, forKey: .version) - links = try c.decode([ResourceLink].self, forKey: .links) + public init(id: String, kind: String, name: String, state: String, assignmentState: String?, version: Int64, links: [ResourceLink]) throws { + self.id = id + self.kind = kind + self.name = name + self.state = state + self.assignmentState = assignmentState + self.version = version + self.links = links + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(id: try c.decode(String.self, forKey: .id), kind: try c.decode(String.self, forKey: .kind), name: try c.decode(String.self, forKey: .name), state: try c.decode(String.self, forKey: .state), assignmentState: try c.decodeIfPresent(String.self, forKey: .assignmentState), version: try c.decode(Int64.self, forKey: .version), links: try c.decode([ResourceLink].self, forKey: .links)) + } + + public func validate() throws { + if self.id.isEmpty { throw ContractValidationError(field: "id", code: "required") } + if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } + if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } + if self.kind.isEmpty { throw ContractValidationError(field: "kind", code: "required") } + if !self.kind.isEmpty && self.kind.utf8.count < 1 { throw ContractValidationError(field: "kind", code: "min_length") } + if self.kind.utf8.count > 64 { throw ContractValidationError(field: "kind", code: "max_length") } + if self.name.isEmpty { throw ContractValidationError(field: "name", code: "required") } + if !self.name.isEmpty && self.name.utf8.count < 1 { throw ContractValidationError(field: "name", code: "min_length") } + if self.name.utf8.count > 256 { throw ContractValidationError(field: "name", code: "max_length") } + if self.state.isEmpty { throw ContractValidationError(field: "state", code: "required") } + if !self.state.isEmpty && self.state.utf8.count < 1 { throw ContractValidationError(field: "state", code: "min_length") } + if self.state.utf8.count > 64 { throw ContractValidationError(field: "state", code: "max_length") } + if let value = self.assignmentState { + if value.utf8.count > 64 { throw ContractValidationError(field: "assignment_state", code: "max_length") } + } + if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } + if self.links.count > 16 { throw ContractValidationError(field: "links", code: "max_items") } + for item in self.links { try item.validate() } + } + + 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) } } public struct ResourceLink: Codable, Equatable { @@ -784,12 +1567,32 @@ public struct ResourceLink: Codable, Equatable { case version = "version" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - type = try c.decode(String.self, forKey: .type) - id = try c.decode(String.self, forKey: .id) - version = try c.decode(Int64.self, forKey: .version) + public init(type: String, id: String, version: Int64) throws { + self.type = type + self.id = id + self.version = version + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(type: try c.decode(String.self, forKey: .type), id: try c.decode(String.self, forKey: .id), version: try c.decode(Int64.self, forKey: .version)) + } + + public func validate() throws { + if self.type.isEmpty { throw ContractValidationError(field: "type", code: "required") } + if !self.type.isEmpty && self.type.utf8.count < 1 { throw ContractValidationError(field: "type", code: "min_length") } + if self.type.utf8.count > 64 { throw ContractValidationError(field: "type", code: "max_length") } + if self.id.isEmpty { throw ContractValidationError(field: "id", code: "required") } + if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } + if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } + if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } + } + + 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) } } public struct ResourceList: Codable, Equatable { @@ -802,12 +1605,30 @@ public struct ResourceList: Codable, Equatable { case page = "page" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - assignedDesktops = try c.decode([AssignedDesktop].self, forKey: .assignedDesktops) - entitledPools = try c.decode([EntitledPool].self, forKey: .entitledPools) - page = try c.decode(PageInfo.self, forKey: .page) + public init(assignedDesktops: [AssignedDesktop], entitledPools: [EntitledPool], page: PageInfo) throws { + self.assignedDesktops = assignedDesktops + self.entitledPools = entitledPools + self.page = page + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(assignedDesktops: try c.decode([AssignedDesktop].self, forKey: .assignedDesktops), entitledPools: try c.decode([EntitledPool].self, forKey: .entitledPools), page: try c.decode(PageInfo.self, forKey: .page)) + } + + public func validate() throws { + if self.assignedDesktops.count > 100 { throw ContractValidationError(field: "assigned_desktops", code: "max_items") } + for item in self.assignedDesktops { try item.validate() } + if self.entitledPools.count > 100 { throw ContractValidationError(field: "entitled_pools", code: "max_items") } + for item in self.entitledPools { try item.validate() } + try self.page.validate() + } + + 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) } } public struct SessionAuthority: Codable, Equatable { @@ -832,18 +1653,49 @@ public struct SessionAuthority: Codable, Equatable { case providerIdentity = "provider_identity" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - sessionId = try c.decode(String.self, forKey: .sessionId) - gatewayId = try c.decode(String.self, forKey: .gatewayId) - audience = try c.decode(String.self, forKey: .audience) - reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence) - expiresAt = try c.decode(String.self, forKey: .expiresAt) - capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities) - providerProfile = try c.decode(String.self, forKey: .providerProfile) - providerIdentity = try c.decode(String.self, forKey: .providerIdentity) + public init(version: String, sessionId: String, gatewayId: String, audience: String, reconnectSequence: Int64, expiresAt: String, capabilities: CapabilityProfile, providerProfile: String, providerIdentity: String) throws { + self.version = version + self.sessionId = sessionId + self.gatewayId = gatewayId + self.audience = audience + self.reconnectSequence = reconnectSequence + self.expiresAt = expiresAt + self.capabilities = capabilities + self.providerProfile = providerProfile + self.providerIdentity = providerIdentity + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), audience: try c.decode(String.self, forKey: .audience), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities), providerProfile: try c.decode(String.self, forKey: .providerProfile), providerIdentity: try c.decode(String.self, forKey: .providerIdentity)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") } + if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") } + if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") } + if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") } + if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") } + if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } + if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") } + if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") } + if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") } + if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + try self.capabilities.validate() + if !["apollo"].contains(self.providerProfile) { throw ContractValidationError(field: "provider_profile", code: "invalid_value") } + if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") } + if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") } + if self.providerIdentity.utf8.count > 256 { throw ContractValidationError(field: "provider_identity", code: "max_length") } + } + + 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) } } public struct SessionRequest: Codable, Equatable { @@ -860,14 +1712,40 @@ public struct SessionRequest: Codable, Equatable { case policySnapshot = "policy_snapshot" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId) - deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId) - poolId = try c.decode(String.self, forKey: .poolId) - idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey) - policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot) + public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) throws { + self.clientDeviceId = clientDeviceId + self.deviceKeyId = deviceKeyId + self.poolId = poolId + self.idempotencyKey = idempotencyKey + self.policySnapshot = policySnapshot + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), poolId: try c.decode(String.self, forKey: .poolId), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot)) + } + + public func validate() throws { + if self.clientDeviceId.isEmpty { throw ContractValidationError(field: "client_device_id", code: "required") } + if !self.clientDeviceId.isEmpty && self.clientDeviceId.utf8.count < 1 { throw ContractValidationError(field: "client_device_id", code: "min_length") } + if self.clientDeviceId.utf8.count > 128 { throw ContractValidationError(field: "client_device_id", code: "max_length") } + if self.deviceKeyId.isEmpty { throw ContractValidationError(field: "device_key_id", code: "required") } + if !self.deviceKeyId.isEmpty && self.deviceKeyId.utf8.count < 1 { throw ContractValidationError(field: "device_key_id", code: "min_length") } + if self.deviceKeyId.utf8.count > 128 { throw ContractValidationError(field: "device_key_id", code: "max_length") } + if self.poolId.isEmpty { throw ContractValidationError(field: "pool_id", code: "required") } + if !self.poolId.isEmpty && self.poolId.utf8.count < 1 { throw ContractValidationError(field: "pool_id", code: "min_length") } + if self.poolId.utf8.count > 128 { throw ContractValidationError(field: "pool_id", code: "max_length") } + if self.idempotencyKey.isEmpty { throw ContractValidationError(field: "idempotency_key", code: "required") } + if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") } + if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") } + try self.policySnapshot.validate() + } + + 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) } } public struct StableError: Codable, Equatable { @@ -882,13 +1760,33 @@ public struct StableError: Codable, Equatable { case retryable = "retryable" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - code = try c.decode(String.self, forKey: .code) - message = try c.decode(String.self, forKey: .message) - retryable = try c.decode(Bool.self, forKey: .retryable) + public init(version: String, code: String, message: String, retryable: Bool) throws { + self.version = version + self.code = code + self.message = message + self.retryable = retryable + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), code: try c.decode(String.self, forKey: .code), message: try c.decode(String.self, forKey: .message), retryable: try c.decode(Bool.self, forKey: .retryable)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.code.isEmpty { throw ContractValidationError(field: "code", code: "required") } + if !self.code.isEmpty && self.code.utf8.count < 1 { throw ContractValidationError(field: "code", code: "min_length") } + if self.code.utf8.count > 128 { throw ContractValidationError(field: "code", code: "max_length") } + if self.message.isEmpty { throw ContractValidationError(field: "message", code: "required") } + if !self.message.isEmpty && self.message.utf8.count < 1 { throw ContractValidationError(field: "message", code: "min_length") } + if self.message.utf8.count > 512 { throw ContractValidationError(field: "message", code: "max_length") } + } + + 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) } } public struct TunnelAdmissionRequest: Codable, Equatable { @@ -911,17 +1809,48 @@ public struct TunnelAdmissionRequest: Codable, Equatable { case capabilities = "capabilities" } - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - version = try c.decode(String.self, forKey: .version) - sessionId = try c.decode(String.self, forKey: .sessionId) - gatewayId = try c.decode(String.self, forKey: .gatewayId) - audience = try c.decode(String.self, forKey: .audience) - grant = try c.decode(String.self, forKey: .grant) - reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence) - clientNonce = try c.decode(String.self, forKey: .clientNonce) - capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities) + public init(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: Int64, clientNonce: String, capabilities: CapabilityProfile) throws { + self.version = version + self.sessionId = sessionId + self.gatewayId = gatewayId + self.audience = audience + self.grant = grant + self.reconnectSequence = reconnectSequence + self.clientNonce = clientNonce + self.capabilities = capabilities + try validate() } + + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } + let c = try decoder.container(keyedBy: CodingKeys.self) + try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), audience: try c.decode(String.self, forKey: .audience), grant: try c.decode(String.self, forKey: .grant), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), clientNonce: try c.decode(String.self, forKey: .clientNonce), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities)) + } + + public func validate() throws { + if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") } + if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") } + if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") } + if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") } + if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") } + if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") } + if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") } + if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") } + if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") } + if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") } + if self.grant.isEmpty { throw ContractValidationError(field: "grant", code: "required") } + if !self.grant.isEmpty && self.grant.utf8.count < 43 { throw ContractValidationError(field: "grant", code: "min_length") } + if self.grant.utf8.count > 256 { throw ContractValidationError(field: "grant", code: "max_length") } + if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } + if self.clientNonce.isEmpty { throw ContractValidationError(field: "client_nonce", code: "required") } + if !self.clientNonce.isEmpty && self.clientNonce.utf8.count < 16 { throw ContractValidationError(field: "client_nonce", code: "min_length") } + if self.clientNonce.utf8.count > 128 { throw ContractValidationError(field: "client_nonce", code: "max_length") } + try self.capabilities.validate() + } + + 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) } } public struct VersionNegotiation: Codable, Equatable { @@ -932,9 +1861,37 @@ public struct VersionNegotiation: Codable, Equatable { case features = "features" } + public init(supportedVersions: [String], features: [String]) throws { + self.supportedVersions = supportedVersions + self.features = features + try validate() + } + public init(from decoder: Decoder) throws { + let all = try decoder.container(keyedBy: AnyCodingKey.self) + for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } let c = try decoder.container(keyedBy: CodingKeys.self) - supportedVersions = try c.decode([String].self, forKey: .supportedVersions) - features = try c.decode([String].self, forKey: .features) + try self.init(supportedVersions: try c.decode([String].self, forKey: .supportedVersions), features: try c.decode([String].self, forKey: .features)) + } + + public func validate() throws { + if self.supportedVersions.count < 1 { throw ContractValidationError(field: "supported_versions", code: "min_items") } + if self.supportedVersions.count > 3 { throw ContractValidationError(field: "supported_versions", code: "max_items") } + if self.features.count > 64 { throw ContractValidationError(field: "features", code: "max_items") } + } + + 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) } +} + +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 } } diff --git a/tests/go/protocol_test.go b/tests/go/protocol_test.go index 8291bae..0a0ae5c 100644 --- a/tests/go/protocol_test.go +++ b/tests/go/protocol_test.go @@ -43,12 +43,35 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) { } for _, invalid := range []string{ strings.Replace(registration, `"version":"1"`, `"version":"2"`, 1), + strings.Replace(registration, `"version":"1"`, `"version":"0"`, 1), strings.Replace(registration, `"features":["datagram.media"]`, `"features":["datagram.media"],"provider_url":"https://provider.invalid"`, 1), } { if _, err := protocol.DecodeGatewayRegistration([]byte(invalid)); err == nil { t.Fatalf("invalid gateway registration accepted: %s", invalid) } } + if _, err := protocol.DecodeGatewayRegistration([]byte("{")); err == nil { + t.Fatal("DecodeGatewayRegistration accepted malformed JSON") + } +} + +func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) { + registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}` + if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil { + t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds") + } +} + +func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) { + first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: "h264-opus"} + if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || got != first { + t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err) + } + second := first + second.ClientDecode = "hevc-opus" + if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil { + t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile") + } } func TestSessionAuthorityRejectsProviderRoute(t *testing.T) { diff --git a/tools/generate.py b/tools/generate.py index 2b579b1..814bc94 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -151,6 +151,12 @@ def go_validation(definition: dict[str, Any]) -> list[str]: 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 @@ -235,6 +241,20 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str, 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", + "}", + "", + ]) # 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"') @@ -272,6 +292,58 @@ def swift_type(prop: dict[str, Any]) -> str: 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.", @@ -282,6 +354,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";', "pub type JsonObject = std::collections::BTreeMap;", "", + "#[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] @@ -292,11 +368,103 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili typ = rust_type(prop) if prop_name not in required: typ = f"Option<{typ}>" - out.append(f" pub {field}: {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 {{") + 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} }}") + out.extend(["}", ""]) + out.extend([ + "pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result {", + " 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.", @@ -306,6 +474,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil 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): @@ -320,16 +490,46 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil out.append(" enum CodingKeys: String, CodingKey {") for prop_name in definition.get("properties", {}): out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"") - out.extend([" }", "", " public init(from decoder: Decoder) throws {",]) + 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: - out.append(f" {field} = try c.decode({typ}.self, forKey: .{field})") + decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})") else: - out.append(f" {field} = try c.decodeIfPresent({typ}.self, forKey: .{field})") - out.extend([" }", "}", ""]) + 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 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) diff --git a/tools/test_generated_contracts.py b/tools/test_generated_contracts.py new file mode 100644 index 0000000..dd1fb3f --- /dev/null +++ b/tools/test_generated_contracts.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Compile and exercise strict generated Swift and Rust gateway contracts.""" + +from __future__ import annotations + +import pathlib +import shutil +import subprocess +import tempfile + + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def run(command: list[str], directory: pathlib.Path) -> None: + result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False) + if result.returncode != 0: + raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr)) + + +def run_failure(command: list[str], directory: pathlib.Path, expected: str) -> None: + result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False) + if result.returncode == 0 or expected not in result.stdout + result.stderr: + raise RuntimeError("expected failure: %s\n%s%s" % (" ".join(command), result.stdout, result.stderr)) + + +def main() -> int: + with tempfile.TemporaryDirectory(prefix="versevdi-generated-contracts-") as temporary: + workspace = pathlib.Path(temporary) + swift = workspace / "main.swift" + swift.write_text( + """import Foundation + +let capability = try CapabilityProfile( + transport: "quic-tls13", framing: "datagram-v1", media: "encoded", + audio: "encoded", sourceRateControl: "server", clientDecode: "h264-opus" +) +let request = try TunnelAdmissionRequest( + version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience", + grant: String(repeating: "g", count: 43), reconnectSequence: 0, + clientNonce: String(repeating: "n", count: 16), capabilities: capability +) +_ = request +let incompatible = try CapabilityProfile( + transport: "quic-tls13", framing: "datagram-v1", media: "encoded", + audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus" +) +do { + guard try CapabilityProfile.intersection([capability, capability]) == capability else { + fatalError("matching capability profiles did not intersect") + } +} catch { fatalError("matching capability profiles did not intersect") } +do { + _ = try CapabilityProfile.intersection([capability, incompatible]) + fatalError("profiles without overlap were accepted") +} catch { } +let valid = try request.encodeJSON() +var unsupported = try JSONSerialization.jsonObject(with: valid) as! [String: Any] +unsupported["version"] = "2" +var downgrade = try JSONSerialization.jsonObject(with: valid) as! [String: Any] +downgrade["version"] = "0" +var unknown = try JSONSerialization.jsonObject(with: valid) as! [String: Any] +unknown["unknown"] = true +for invalid in [ + try JSONSerialization.data(withJSONObject: unsupported), + try JSONSerialization.data(withJSONObject: downgrade), + try JSONSerialization.data(withJSONObject: unknown), + Data("{".utf8), + valid + Data(" {}".utf8), +] { + do { + _ = try TunnelAdmissionRequest.decodeJSON(invalid) + fatalError("invalid tunnel admission request was accepted") + } catch { } +} +do { + _ = try AllocationPolicy( + minimumKbps: 100, targetKbps: 50, maximumKbps: 25, tier: "standard", + audience: "audience", protocolValue: "verse", protocolVersion: 1, + grantTtlSeconds: 60, reservationLeaseSeconds: 300 + ) + fatalError("invalid allocation bounds were accepted") +} catch { } +""", + encoding="utf-8", + ) + run(["swiftc", str(ROOT / "gen/swift/Protocol.swift"), str(swift), "-o", str(workspace / "swift-contracts")], ROOT) + run([str(workspace / "swift-contracts")], ROOT) + + rust = workspace / "protocol.rs" + shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust) + with rust.open("a", encoding="utf-8") as output: + output.write( + """ +fn main() { + let capabilities = CapabilityProfile::new( + "quic-tls13".into(), "datagram-v1".into(), "encoded".into(), + "encoded".into(), "server".into(), "h264-opus".into(), + ).unwrap(); + assert!(TunnelAdmissionRequest::new( + "2".into(), "session".into(), "gateway".into(), "audience".into(), + "g".repeat(43), 0, "n".repeat(16), capabilities.clone(), + ).is_err()); + assert!(TunnelAdmissionRequest::new( + "0".into(), "session".into(), "gateway".into(), "audience".into(), + "g".repeat(43), 0, "n".repeat(16), capabilities.clone(), + ).is_err()); + assert!(TunnelAdmissionRequest::new( + "1".into(), "session".into(), "gateway".into(), "audience".into(), + "g".repeat(43), 0, "short".into(), capabilities.clone(), + ).is_err()); + assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok()); + let incompatible = CapabilityProfile::new( + "quic-tls13".into(), "datagram-v1".into(), "encoded".into(), + "encoded".into(), "server".into(), "hevc-opus".into(), + ).unwrap(); + assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err()); + assert!(AllocationPolicy::new( + 100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300, + ).is_err()); +} +""" + ) + run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT) + run([str(workspace / "rust-contracts")], ROOT) + rust_unknown = workspace / "unknown.rs" + shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust_unknown) + with rust_unknown.open("a", encoding="utf-8") as output: + output.write("\nfn main() { let _ = CapabilityProfile { unknown: String::new() }; }\n") + run_failure(["rustc", str(rust_unknown), "-o", str(workspace / "rust-unknown")], ROOT, "no field named `unknown`") + print("Generated strict contract checks passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())