//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `vtc/website/deploy`. Version: `0.1`.
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
/// Error types.
pub mod error {
/// Error from a `TryFrom` or `FromStr` implementation.
pub struct ConversionError(::std::borrow::Cow<'static, str>);
impl ::std::error::Error for ConversionError {}
impl ::std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::fmt::Debug for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Debug::fmt(&self.0, f)
}
}
impl From<&'static str> for ConversionError {
fn from(value: &'static str) -> Self {
Self(value.into())
}
}
impl From<String> for ConversionError {
fn from(value: String) -> Self {
Self(value.into())
}
}
}
///Lowercase hex SHA-256 of the whole bundle's bytes. Kept in the hex form the 1.0 descriptor published rather than moved to DigestMultibase, because it is an unchanged member of an existing descriptor and re-encoding it would break every stream producer for no gain in what it checks. For a chunked transfer it is the check over the reassembled bundle, applied after every chunk has verified individually, so that a correct set of chunks assembled in the wrong order is still caught.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ExpectedSha256",
/// "description": "Lowercase hex SHA-256 of the whole bundle's bytes. Kept in the hex form the 1.0 descriptor published rather than moved to DigestMultibase, because it is an unchanged member of an existing descriptor and re-encoding it would break every stream producer for no gain in what it checks. For a chunked transfer it is the check over the reassembled bundle, applied after every chunk has verified individually, so that a correct set of chunks assembled in the wrong order is still caught.",
/// "type": "string",
/// "pattern": "^[0-9a-f]{64}$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ExpectedSha256(::std::string::String);
impl ::std::ops::Deref for ExpectedSha256 {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ExpectedSha256> for ::std::string::String {
fn from(value: ExpectedSha256) -> Self {
value.0
}
}
impl ::std::str::FromStr for ExpectedSha256 {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| ::regress::Regex::new("^[0-9a-f]{64}$").unwrap());
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[0-9a-f]{64}$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ExpectedSha256 {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ExpectedSha256 {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ExpectedSha256 {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ExpectedSha256 {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Total byte count of the bundle. A zero-length bundle is not a degenerate success — nothing was serialized — so the floor is 1.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ExpectedSizeBytes",
/// "description": "Total byte count of the bundle. A zero-length bundle is not a degenerate success — nothing was serialized — so the floor is 1.",
/// "type": "integer",
/// "minimum": 1.0
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct ExpectedSizeBytes(pub ::std::num::NonZeroU64);
impl ::std::ops::Deref for ExpectedSizeBytes {
type Target = ::std::num::NonZeroU64;
fn deref(&self) -> &::std::num::NonZeroU64 {
&self.0
}
}
impl ::std::convert::From<ExpectedSizeBytes> for ::std::num::NonZeroU64 {
fn from(value: ExpectedSizeBytes) -> Self {
value.0
}
}
impl ::std::convert::From<::std::num::NonZeroU64> for ExpectedSizeBytes {
fn from(value: ::std::num::NonZeroU64) -> Self {
Self(value)
}
}
impl ::std::str::FromStr for ExpectedSizeBytes {
type Err = <::std::num::NonZeroU64 as ::std::str::FromStr>::Err;
fn from_str(value: &str) -> ::std::result::Result<Self, Self::Err> {
Ok(Self(value.parse()?))
}
}
impl ::std::convert::TryFrom<&str> for ExpectedSizeBytes {
type Error = <::std::num::NonZeroU64 as ::std::str::FromStr>::Err;
fn try_from(value: &str) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::convert::TryFrom<String> for ExpectedSizeBytes {
type Error = <::std::num::NonZeroU64 as ::std::str::FromStr>::Err;
fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::fmt::Display for ExpectedSizeBytes {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
self.0.fmt(f)
}
}
///Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Ext",
/// "description": "Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.",
/// "type": "object",
/// "minProperties": 1,
/// "additionalProperties": true,
/// "propertyNames": {
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
/// }
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct Ext(pub ::std::collections::HashMap<ExtKey, ::serde_json::Value>);
impl ::std::ops::Deref for Ext {
type Target = ::std::collections::HashMap<ExtKey, ::serde_json::Value>;
fn deref(&self) -> &::std::collections::HashMap<ExtKey, ::serde_json::Value> {
&self.0
}
}
impl ::std::convert::From<Ext> for ::std::collections::HashMap<ExtKey, ::serde_json::Value> {
fn from(value: Ext) -> Self {
value.0
}
}
impl ::std::convert::From<::std::collections::HashMap<ExtKey, ::serde_json::Value>> for Ext {
fn from(value: ::std::collections::HashMap<ExtKey, ::serde_json::Value>) -> Self {
Self(value)
}
}
///`ExtKey`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "type": "string",
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ExtKey(::std::string::String);
impl ::std::ops::Deref for ExtKey {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ExtKey> for ::std::string::String {
fn from(value: ExtKey) -> Self {
value.0
}
}
impl ::std::str::FromStr for ExtKey {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| {
::regress::Regex::new("^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$").unwrap()
});
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ExtKey {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Publish a staged bundle upload. The outer document members are owned by the framework — SPEC §6.3.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "$id": "https://trusttasks.org/spec/vtc/website/deploy/0.1",
/// "title": "Payload",
/// "description": "Publish a staged bundle upload. The outer document members are owned by the framework — SPEC §6.3.",
/// "type": "object",
/// "required": [
/// "uploadId"
/// ],
/// "properties": {
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "uploadId": {
/// "$ref": "#/definitions/UploadId"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Payload {
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
#[serde(rename = "uploadId")]
pub upload_id: UploadId,
}
impl Payload {
pub fn builder() -> builder::Payload {
Default::default()
}
}
///`Response`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Response",
/// "type": "object",
/// "required": [
/// "bundleSha256",
/// "bundleSizeBytes",
/// "deployMode",
/// "prunedGenerations",
/// "targetGeneration"
/// ],
/// "properties": {
/// "bundleSha256": {
/// "$ref": "#/definitions/ExpectedSha256"
/// },
/// "bundleSizeBytes": {
/// "$ref": "#/definitions/ExpectedSizeBytes"
/// },
/// "deployMode": {
/// "description": "How the site was replaced.",
/// "type": "string",
/// "enum": [
/// "live",
/// "managed"
/// ]
/// },
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "prunedGenerations": {
/// "description": "Old generations removed; 0 in live mode.",
/// "type": "integer",
/// "minimum": 0.0
/// },
/// "targetGeneration": {
/// "description": "The generation created; 0 in live mode.",
/// "type": "integer",
/// "minimum": 0.0
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "response"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Response {
#[serde(rename = "bundleSha256")]
pub bundle_sha256: ExpectedSha256,
#[serde(rename = "bundleSizeBytes")]
pub bundle_size_bytes: ExpectedSizeBytes,
///How the site was replaced.
#[serde(rename = "deployMode")]
pub deploy_mode: ResponseDeployMode,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
///Old generations removed; 0 in live mode.
#[serde(rename = "prunedGenerations")]
pub pruned_generations: u64,
///The generation created; 0 in live mode.
#[serde(rename = "targetGeneration")]
pub target_generation: u64,
}
impl Response {
pub fn builder() -> builder::Response {
Default::default()
}
}
///How the site was replaced.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "How the site was replaced.",
/// "type": "string",
/// "enum": [
/// "live",
/// "managed"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ResponseDeployMode {
#[serde(rename = "live")]
Live,
#[serde(rename = "managed")]
Managed,
}
impl ::std::fmt::Display for ResponseDeployMode {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Live => f.write_str("live"),
Self::Managed => f.write_str("managed"),
}
}
}
impl ::std::str::FromStr for ResponseDeployMode {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"live" => Ok(Self::Live),
"managed" => Ok(Self::Managed),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ResponseDeployMode {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ResponseDeployMode {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ResponseDeployMode {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///Handle for one upload across begin, chunk, commit and abort, and — for a bundle — deploy. Recipient-generated and unguessable, which is what lets a reference to somebody else's upload be answered as not-found without confirming it exists. Opaque: a producer quotes what it was given.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "UploadId",
/// "description": "Handle for one upload across begin, chunk, commit and abort, and — for a bundle — deploy. Recipient-generated and unguessable, which is what lets a reference to somebody else's upload be answered as not-found without confirming it exists. Opaque: a producer quotes what it was given.",
/// "type": "string",
/// "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct UploadId(::std::string::String);
impl ::std::ops::Deref for UploadId {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<UploadId> for ::std::string::String {
fn from(value: UploadId) -> Self {
value.0
}
}
impl ::std::str::FromStr for UploadId {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| {
::regress::Regex::new(
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
)
.unwrap()
});
if PATTERN.find(value).is_none() {
return Err(
"doesn't match pattern \"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$\""
.into(),
);
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for UploadId {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for UploadId {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for UploadId {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for UploadId {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
/// Types for composing complex structures.
pub mod builder {
#[derive(Clone, Debug)]
pub struct Payload {
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
upload_id: ::std::result::Result<super::UploadId, ::std::string::String>,
}
impl ::std::default::Default for Payload {
fn default() -> Self {
Self {
ext: Ok(Default::default()),
upload_id: Err("no value supplied for upload_id".to_string()),
}
}
}
impl Payload {
pub fn ext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::Ext>>,
T::Error: ::std::fmt::Display,
{
self.ext = value
.try_into()
.map_err(|e| format!("error converting supplied value for ext: {e}"));
self
}
pub fn upload_id<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::UploadId>,
T::Error: ::std::fmt::Display,
{
self.upload_id = value
.try_into()
.map_err(|e| format!("error converting supplied value for upload_id: {e}"));
self
}
}
impl ::std::convert::TryFrom<Payload> for super::Payload {
type Error = super::error::ConversionError;
fn try_from(value: Payload) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
ext: value.ext?,
upload_id: value.upload_id?,
})
}
}
impl ::std::convert::From<super::Payload> for Payload {
fn from(value: super::Payload) -> Self {
Self {
ext: Ok(value.ext),
upload_id: Ok(value.upload_id),
}
}
}
#[derive(Clone, Debug)]
pub struct Response {
bundle_sha256: ::std::result::Result<super::ExpectedSha256, ::std::string::String>,
bundle_size_bytes: ::std::result::Result<super::ExpectedSizeBytes, ::std::string::String>,
deploy_mode: ::std::result::Result<super::ResponseDeployMode, ::std::string::String>,
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
pruned_generations: ::std::result::Result<u64, ::std::string::String>,
target_generation: ::std::result::Result<u64, ::std::string::String>,
}
impl ::std::default::Default for Response {
fn default() -> Self {
Self {
bundle_sha256: Err("no value supplied for bundle_sha256".to_string()),
bundle_size_bytes: Err("no value supplied for bundle_size_bytes".to_string()),
deploy_mode: Err("no value supplied for deploy_mode".to_string()),
ext: Ok(Default::default()),
pruned_generations: Err("no value supplied for pruned_generations".to_string()),
target_generation: Err("no value supplied for target_generation".to_string()),
}
}
}
impl Response {
pub fn bundle_sha256<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ExpectedSha256>,
T::Error: ::std::fmt::Display,
{
self.bundle_sha256 = value
.try_into()
.map_err(|e| format!("error converting supplied value for bundle_sha256: {e}"));
self
}
pub fn bundle_size_bytes<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ExpectedSizeBytes>,
T::Error: ::std::fmt::Display,
{
self.bundle_size_bytes = value
.try_into()
.map_err(|e| format!("error converting supplied value for bundle_size_bytes: {e}"));
self
}
pub fn deploy_mode<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ResponseDeployMode>,
T::Error: ::std::fmt::Display,
{
self.deploy_mode = value
.try_into()
.map_err(|e| format!("error converting supplied value for deploy_mode: {e}"));
self
}
pub fn ext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::Ext>>,
T::Error: ::std::fmt::Display,
{
self.ext = value
.try_into()
.map_err(|e| format!("error converting supplied value for ext: {e}"));
self
}
pub fn pruned_generations<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<u64>,
T::Error: ::std::fmt::Display,
{
self.pruned_generations = value.try_into().map_err(|e| {
format!("error converting supplied value for pruned_generations: {e}")
});
self
}
pub fn target_generation<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<u64>,
T::Error: ::std::fmt::Display,
{
self.target_generation = value
.try_into()
.map_err(|e| format!("error converting supplied value for target_generation: {e}"));
self
}
}
impl ::std::convert::TryFrom<Response> for super::Response {
type Error = super::error::ConversionError;
fn try_from(value: Response) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
bundle_sha256: value.bundle_sha256?,
bundle_size_bytes: value.bundle_size_bytes?,
deploy_mode: value.deploy_mode?,
ext: value.ext?,
pruned_generations: value.pruned_generations?,
target_generation: value.target_generation?,
})
}
}
impl ::std::convert::From<super::Response> for Response {
fn from(value: super::Response) -> Self {
Self {
bundle_sha256: Ok(value.bundle_sha256),
bundle_size_bytes: Ok(value.bundle_size_bytes),
deploy_mode: Ok(value.deploy_mode),
ext: Ok(value.ext),
pruned_generations: Ok(value.pruned_generations),
target_generation: Ok(value.target_generation),
}
}
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/vtc/website/deploy/0.1";
const IS_PROOF_REQUIRED: bool = true;
const IS_ISSUED_AT_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
const PAYLOAD_SCHEMA: Option<&'static str> = Some(
"{\n \"$defs\": {\n \"ExpectedSha256\": {\n \"description\": \"Lowercase hex SHA-256 of the whole bundle's bytes. Kept in the hex form the 1.0 descriptor published rather than moved to DigestMultibase, because it is an unchanged member of an existing descriptor and re-encoding it would break every stream producer for no gain in what it checks. For a chunked transfer it is the check over the reassembled bundle, applied after every chunk has verified individually, so that a correct set of chunks assembled in the wrong order is still caught.\",\n \"pattern\": \"^[0-9a-f]{64}$\",\n \"title\": \"ExpectedSha256\",\n \"type\": \"string\"\n },\n \"ExpectedSizeBytes\": {\n \"description\": \"Total byte count of the bundle. A zero-length bundle is not a degenerate success — nothing was serialized — so the floor is 1.\",\n \"minimum\": 1,\n \"title\": \"ExpectedSizeBytes\",\n \"type\": \"integer\"\n },\n \"Ext\": {\n \"additionalProperties\": true,\n \"description\": \"Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.\",\n \"minProperties\": 1,\n \"propertyNames\": {\n \"pattern\": \"^[a-z][a-z0-9-]*(\\\\.[a-z0-9-]+)+$\"\n },\n \"title\": \"Ext\",\n \"type\": \"object\"\n },\n \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"properties\": {\n \"bundleSha256\": {\n \"$ref\": \"#/$defs/ExpectedSha256\"\n },\n \"bundleSizeBytes\": {\n \"$ref\": \"#/$defs/ExpectedSizeBytes\"\n },\n \"deployMode\": {\n \"description\": \"How the site was replaced.\",\n \"enum\": [\n \"live\",\n \"managed\"\n ],\n \"type\": \"string\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"prunedGenerations\": {\n \"description\": \"Old generations removed; 0 in live mode.\",\n \"minimum\": 0,\n \"type\": \"integer\"\n },\n \"targetGeneration\": {\n \"description\": \"The generation created; 0 in live mode.\",\n \"minimum\": 0,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"deployMode\",\n \"bundleSha256\",\n \"bundleSizeBytes\",\n \"targetGeneration\",\n \"prunedGenerations\"\n ],\n \"title\": \"VTC Website — Deploy — response payload\",\n \"type\": \"object\"\n },\n \"UploadId\": {\n \"description\": \"Handle for one upload across begin, chunk, commit and abort, and — for a bundle — deploy. Recipient-generated and unguessable, which is what lets a reference to somebody else's upload be answered as not-found without confirming it exists. Opaque: a producer quotes what it was given.\",\n \"pattern\": \"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$\",\n \"title\": \"UploadId\",\n \"type\": \"string\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/vtc/website/deploy/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"description\": \"Publish a staged bundle upload. The outer document members are owned by the framework — SPEC §6.3.\",\n \"properties\": {\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"uploadId\": {\n \"$ref\": \"#/$defs/UploadId\"\n }\n },\n \"required\": [\n \"uploadId\"\n ],\n \"title\": \"VTC Website — Deploy — payload\",\n \"type\": \"object\"\n}\n",
);
}
impl crate::Payload for Response {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/vtc/website/deploy/0.1#response";
const IS_PROOF_REQUIRED: bool = true;
const IS_ISSUED_AT_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
const PAYLOAD_SCHEMA: Option<&'static str> = Some(
"{\n \"$defs\": {\n \"ExpectedSha256\": {\n \"description\": \"Lowercase hex SHA-256 of the whole bundle's bytes. Kept in the hex form the 1.0 descriptor published rather than moved to DigestMultibase, because it is an unchanged member of an existing descriptor and re-encoding it would break every stream producer for no gain in what it checks. For a chunked transfer it is the check over the reassembled bundle, applied after every chunk has verified individually, so that a correct set of chunks assembled in the wrong order is still caught.\",\n \"pattern\": \"^[0-9a-f]{64}$\",\n \"title\": \"ExpectedSha256\",\n \"type\": \"string\"\n },\n \"ExpectedSizeBytes\": {\n \"description\": \"Total byte count of the bundle. A zero-length bundle is not a degenerate success — nothing was serialized — so the floor is 1.\",\n \"minimum\": 1,\n \"title\": \"ExpectedSizeBytes\",\n \"type\": \"integer\"\n },\n \"Ext\": {\n \"additionalProperties\": true,\n \"description\": \"Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.\",\n \"minProperties\": 1,\n \"propertyNames\": {\n \"pattern\": \"^[a-z][a-z0-9-]*(\\\\.[a-z0-9-]+)+$\"\n },\n \"title\": \"Ext\",\n \"type\": \"object\"\n },\n \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"properties\": {\n \"bundleSha256\": {\n \"$ref\": \"#/$defs/ExpectedSha256\"\n },\n \"bundleSizeBytes\": {\n \"$ref\": \"#/$defs/ExpectedSizeBytes\"\n },\n \"deployMode\": {\n \"description\": \"How the site was replaced.\",\n \"enum\": [\n \"live\",\n \"managed\"\n ],\n \"type\": \"string\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"prunedGenerations\": {\n \"description\": \"Old generations removed; 0 in live mode.\",\n \"minimum\": 0,\n \"type\": \"integer\"\n },\n \"targetGeneration\": {\n \"description\": \"The generation created; 0 in live mode.\",\n \"minimum\": 0,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"deployMode\",\n \"bundleSha256\",\n \"bundleSizeBytes\",\n \"targetGeneration\",\n \"prunedGenerations\"\n ],\n \"title\": \"VTC Website — Deploy — response payload\",\n \"type\": \"object\"\n },\n \"UploadId\": {\n \"description\": \"Handle for one upload across begin, chunk, commit and abort, and — for a bundle — deploy. Recipient-generated and unguessable, which is what lets a reference to somebody else's upload be answered as not-found without confirming it exists. Opaque: a producer quotes what it was given.\",\n \"pattern\": \"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$\",\n \"title\": \"UploadId\",\n \"type\": \"string\"\n }\n },\n \"$ref\": \"#/$defs/Response\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\n}\n",
);
}
impl crate::RequestPayload for Payload {
type Response = Response;
}
/// The extended error codes this specification declares (SPEC §7.3 item 9,
/// §8.5), in declaration order. Empty when it declares none.
pub const ERROR_CODES: &[crate::DeclaredErrorCode] =
&[error_codes::NOT_FOUND, error_codes::BUNDLE_REFUSED];
/// One constant per extended error code this specification declares
/// (SPEC §7.3 item 9), named for its local part.
///
/// Emit these rather than a string literal: the code is read from the
/// specification, so it cannot name a code the specification never
/// declared.
pub mod error_codes {
/// `vtc/website/deploy:notFound`
///
/// No committed, staged bundle upload with this id exists that the caller may deploy — including a file upload, which commit already wrote.
///
/// Declared `retryable: false`.
pub const NOT_FOUND: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/deploy:notFound",
retryable: false,
};
/// `vtc/website/deploy:bundleRefused`
///
/// The bundle cannot be published: it is not a gzip-compressed tar, an entry's path escapes the site root or names a hidden or blocklisted file, or it expands past the community's decompression limit. Checked on every entry before anything is extracted, so a refused bundle changes nothing. `details.reason` classifies it.
///
/// Declared `retryable: false`.
pub const BUNDLE_REFUSED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/deploy:bundleRefused",
retryable: false,
};
}
#[cfg(test)]
mod conformance {
//! Round-trip tests harvested from the spec's `spec.md`,
//! plus a `rejects_invalid_examples` test for any fixtures
//! in `payload.invalid-examples.json` (validate feature).
#[test]
fn request_example_1() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-00000000000b\",\n \"type\": \"https://trusttasks.org/spec/vtc/website/deploy/0.1#request\",\n \"issuer\": \"did:example:administrator\",\n \"recipient\": \"did:example:community\",\n \"issuedAt\": \"2026-09-28T10:00:00Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000001ff\",\n \"payload\": {\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\"\n }\n}\n";
let doc: crate::TrustTask<super::Payload> =
serde_json::from_str(JSON).expect("deserialize request example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "request example failed round-trip");
}
#[test]
fn response_example_1() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-00000000000c\",\n \"type\": \"https://trusttasks.org/spec/vtc/website/deploy/0.1#response\",\n \"issuer\": \"did:example:community\",\n \"recipient\": \"did:example:administrator\",\n \"issuedAt\": \"2026-09-28T10:08:00Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000001ff\",\n \"payload\": {\n \"deployMode\": \"managed\",\n \"bundleSha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"bundleSizeBytes\": 300000,\n \"targetGeneration\": 7,\n \"prunedGenerations\": 1\n }\n}\n";
let doc: crate::TrustTask<super::Response> =
serde_json::from_str(JSON).expect("deserialize response example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "response example failed round-trip");
}
/// Each fixture in `payload.invalid-examples.json` MUST be
/// rejected by at least one of: serde deserialization, or
/// JSON-Schema validation under the `validate` feature. The
/// fixture file documents the producer-side bug class that
/// each payload exemplifies; this generated test pins it.
#[cfg(feature = "validate")]
#[test]
fn rejects_invalid_examples() {
use crate::validate::ValidatedPayload;
let fixtures: &[(&str, &str)] = &[
(
"The bundle inline — bundles move as chunked uploads, never in the deploy document.",
"{\n \"bundle\": \"H4sIAAAAAAAA\",\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\"\n}",
),
(
"A deploy mode in the request — the mode is the community's configuration.",
"{\n \"deployMode\": \"live\",\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\"\n}",
),
];
for (i, (note, raw)) in fixtures.iter().enumerate() {
let value: serde_json::Value = match serde_json::from_str(raw) {
Ok(v) => v,
Err(_) => continue,
};
let serde_ok = serde_json::from_value::<super::Payload>(value.clone()).is_ok();
let schema_ok = super::Payload::validate_value(&value).is_ok();
assert!(
!(serde_ok && schema_ok),
"invalid-example #{} ({:?}) was accepted by both serde and JSON Schema; \
the fixture's stated failure class is no longer caught:\n{}",
i + 1,
note,
raw
);
}
}
/// Each `"variant": "response"` fixture in
/// `payload.invalid-examples.json` MUST be rejected as a
/// response payload by at least one of: serde deserialization
/// into `Response`, or JSON-Schema validation against the
/// `$anchor: "response"` sub-schema (validate feature).
#[cfg(feature = "validate")]
#[test]
fn rejects_invalid_response_examples() {
use crate::validate::ValidatedPayload;
let fixtures: &[(&str, &str)] = &[
(
"An unknown deploy mode.",
"{\n \"bundleSha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"bundleSizeBytes\": 1,\n \"deployMode\": \"blueGreen\",\n \"prunedGenerations\": 0,\n \"targetGeneration\": 0\n}",
),
(
"Response without the bundle's hash.",
"{\n \"bundleSizeBytes\": 1,\n \"deployMode\": \"live\",\n \"prunedGenerations\": 0,\n \"targetGeneration\": 0\n}",
),
];
for (i, (note, raw)) in fixtures.iter().enumerate() {
let value: serde_json::Value = match serde_json::from_str(raw) {
Ok(v) => v,
Err(_) => continue,
};
let serde_ok = serde_json::from_value::<super::Response>(value.clone()).is_ok();
let schema_ok = super::Response::validate_value(&value).is_ok();
assert!(
!(serde_ok && schema_ok),
"invalid response example #{} ({:?}) was accepted by both serde and JSON Schema; \
the fixture's stated failure class is no longer caught:\n{}",
i + 1,
note,
raw
);
}
}
}