//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `vtc/website/upload/commit`. 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)
}
}
///After which the bundle is collected: staged bytes discarded, tokens and chunk requests refused. Short by design. For a chunked transfer a recipient may move it later as chunks are exchanged, never past its own ceiling — each chunk response reports the current value.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ExpiresAt",
/// "description": "After which the bundle is collected: staged bytes discarded, tokens and chunk requests refused. Short by design. For a chunked transfer a recipient may move it later as chunks are exchanged, never past its own ceiling — each chunk response reports the current value.",
/// "type": "string",
/// "format": "date-time"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct ExpiresAt(pub ::chrono::DateTime<::chrono::offset::Utc>);
impl ::std::ops::Deref for ExpiresAt {
type Target = ::chrono::DateTime<::chrono::offset::Utc>;
fn deref(&self) -> &::chrono::DateTime<::chrono::offset::Utc> {
&self.0
}
}
impl ::std::convert::From<ExpiresAt> for ::chrono::DateTime<::chrono::offset::Utc> {
fn from(value: ExpiresAt) -> Self {
value.0
}
}
impl ::std::convert::From<::chrono::DateTime<::chrono::offset::Utc>> for ExpiresAt {
fn from(value: ::chrono::DateTime<::chrono::offset::Utc>) -> Self {
Self(value)
}
}
impl ::std::str::FromStr for ExpiresAt {
type Err = <::chrono::DateTime<::chrono::offset::Utc> 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 ExpiresAt {
type Error = <::chrono::DateTime<::chrono::offset::Utc> as ::std::str::FromStr>::Err;
fn try_from(value: &str) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::convert::TryFrom<String> for ExpiresAt {
type Error = <::chrono::DateTime<::chrono::offset::Utc> as ::std::str::FromStr>::Err;
fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::fmt::Display for ExpiresAt {
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())
})
}
}
///Complete an 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/upload/commit/0.1",
/// "title": "Payload",
/// "description": "Complete an 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()
}
}
///Exactly one of `file` (for a `file` target) and `stagedUntil` (for a `bundle` target) is present — a rule over two members that this schema does not express.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Response",
/// "description": "Exactly one of `file` (for a `file` target) and `stagedUntil` (for a `bundle` target) is present — a rule over two members that this schema does not express.",
/// "type": "object",
/// "required": [
/// "sha256",
/// "sizeBytes",
/// "target",
/// "uploadId"
/// ],
/// "properties": {
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "file": {
/// "$ref": "#/definitions/WrittenFile"
/// },
/// "sha256": {
/// "$ref": "#/definitions/ExpectedSha256"
/// },
/// "sizeBytes": {
/// "$ref": "#/definitions/ExpectedSizeBytes"
/// },
/// "stagedUntil": {
/// "$ref": "#/definitions/ExpiresAt"
/// },
/// "target": {
/// "$ref": "#/definitions/UploadTarget"
/// },
/// "uploadId": {
/// "$ref": "#/definitions/UploadId"
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "response"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Response {
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub file: ::std::option::Option<WrittenFile>,
pub sha256: ExpectedSha256,
#[serde(rename = "sizeBytes")]
pub size_bytes: ExpectedSizeBytes,
#[serde(
rename = "stagedUntil",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub staged_until: ::std::option::Option<ExpiresAt>,
pub target: UploadTarget,
#[serde(rename = "uploadId")]
pub upload_id: UploadId,
}
impl Response {
pub fn builder() -> builder::Response {
Default::default()
}
}
///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())
})
}
}
///What the uploaded bytes are for, committed before any byte moves. `file`: one file at `path`, written into the site when the upload commits — the single-file write. `bundle`: a gzip-compressed tar of the whole site, staged when the upload commits and published by vtc/website/deploy. `path` is required for a `file` target and forbidden for a `bundle`, and `ifMatch` is allowed only for a `file` — rules over two members that this schema does not express; a community refuses a violation with `malformedRequest`. `ifMatch` is optimistic concurrency: the write commits only if the file's current content hash is this value.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "UploadTarget",
/// "description": "What the uploaded bytes are for, committed before any byte moves. `file`: one file at `path`, written into the site when the upload commits — the single-file write. `bundle`: a gzip-compressed tar of the whole site, staged when the upload commits and published by vtc/website/deploy. `path` is required for a `file` target and forbidden for a `bundle`, and `ifMatch` is allowed only for a `file` — rules over two members that this schema does not express; a community refuses a violation with `malformedRequest`. `ifMatch` is optimistic concurrency: the write commits only if the file's current content hash is this value.",
/// "type": "object",
/// "required": [
/// "kind"
/// ],
/// "properties": {
/// "ifMatch": {
/// "$ref": "#/definitions/WebsiteEtag"
/// },
/// "kind": {
/// "type": "string",
/// "enum": [
/// "file",
/// "bundle"
/// ]
/// },
/// "path": {
/// "$ref": "#/definitions/WebsitePath"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct UploadTarget {
#[serde(
rename = "ifMatch",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub if_match: ::std::option::Option<WebsiteEtag>,
pub kind: UploadTargetKind,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub path: ::std::option::Option<WebsitePath>,
}
impl UploadTarget {
pub fn builder() -> builder::UploadTarget {
Default::default()
}
}
///`UploadTargetKind`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "type": "string",
/// "enum": [
/// "file",
/// "bundle"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum UploadTargetKind {
#[serde(rename = "file")]
File,
#[serde(rename = "bundle")]
Bundle,
}
impl ::std::fmt::Display for UploadTargetKind {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::File => f.write_str("file"),
Self::Bundle => f.write_str("bundle"),
}
}
}
impl ::std::str::FromStr for UploadTargetKind {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"file" => Ok(Self::File),
"bundle" => Ok(Self::Bundle),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for UploadTargetKind {
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 UploadTargetKind {
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 UploadTargetKind {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///A file's content hash: the lowercase hex SHA-256 of its bytes — the `etag` vtc/website/files/list reports, and the `ETag` a direct read of the path returns, without the HTTP quotes.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "WebsiteEtag",
/// "description": "A file's content hash: the lowercase hex SHA-256 of its bytes — the `etag` vtc/website/files/list reports, and the `ETag` a direct read of the path returns, without the HTTP quotes.",
/// "type": "string",
/// "pattern": "^[0-9a-f]{64}$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct WebsiteEtag(::std::string::String);
impl ::std::ops::Deref for WebsiteEtag {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<WebsiteEtag> for ::std::string::String {
fn from(value: WebsiteEtag) -> Self {
value.0
}
}
impl ::std::str::FromStr for WebsiteEtag {
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 WebsiteEtag {
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 WebsiteEtag {
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 WebsiteEtag {
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 WebsiteEtag {
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())
})
}
}
///A file's path relative to the site root, `/`-separated, with or without a leading `/`. The community resolves it inside the site root and refuses one that escapes the root (`..`), names a hidden file or directory (a segment beginning `.`), carries a blocklisted extension, or contains a control character — the same rules the public read handler applies, so nothing can be written that the site would refuse to serve, and nothing can be read that it would not.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "WebsitePath",
/// "description": "A file's path relative to the site root, `/`-separated, with or without a leading `/`. The community resolves it inside the site root and refuses one that escapes the root (`..`), names a hidden file or directory (a segment beginning `.`), carries a blocklisted extension, or contains a control character — the same rules the public read handler applies, so nothing can be written that the site would refuse to serve, and nothing can be read that it would not.",
/// "type": "string",
/// "maxLength": 1024,
/// "minLength": 1,
/// "pattern": "^[^\\\\]+$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct WebsitePath(::std::string::String);
impl ::std::ops::Deref for WebsitePath {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<WebsitePath> for ::std::string::String {
fn from(value: WebsitePath) -> Self {
value.0
}
}
impl ::std::str::FromStr for WebsitePath {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() > 1024usize {
return Err("longer than 1024 characters".into());
}
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| ::regress::Regex::new("^[^\\\\]+$").unwrap());
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[^\\\\]+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for WebsitePath {
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 WebsitePath {
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 WebsitePath {
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 WebsitePath {
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())
})
}
}
///The file a `file` upload wrote.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "WrittenFile",
/// "description": "The file a `file` upload wrote.",
/// "type": "object",
/// "required": [
/// "etag",
/// "path",
/// "sizeBytes"
/// ],
/// "properties": {
/// "etag": {
/// "$ref": "#/definitions/WebsiteEtag"
/// },
/// "path": {
/// "$ref": "#/definitions/WebsitePath"
/// },
/// "sizeBytes": {
/// "description": "The file's size in bytes.",
/// "type": "integer",
/// "minimum": 1.0
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct WrittenFile {
pub etag: WebsiteEtag,
pub path: WebsitePath,
///The file's size in bytes.
#[serde(rename = "sizeBytes")]
pub size_bytes: ::std::num::NonZeroU64,
}
impl WrittenFile {
pub fn builder() -> builder::WrittenFile {
Default::default()
}
}
/// 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 {
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
file:
::std::result::Result<::std::option::Option<super::WrittenFile>, ::std::string::String>,
sha256: ::std::result::Result<super::ExpectedSha256, ::std::string::String>,
size_bytes: ::std::result::Result<super::ExpectedSizeBytes, ::std::string::String>,
staged_until:
::std::result::Result<::std::option::Option<super::ExpiresAt>, ::std::string::String>,
target: ::std::result::Result<super::UploadTarget, ::std::string::String>,
upload_id: ::std::result::Result<super::UploadId, ::std::string::String>,
}
impl ::std::default::Default for Response {
fn default() -> Self {
Self {
ext: Ok(Default::default()),
file: Ok(Default::default()),
sha256: Err("no value supplied for sha256".to_string()),
size_bytes: Err("no value supplied for size_bytes".to_string()),
staged_until: Ok(Default::default()),
target: Err("no value supplied for target".to_string()),
upload_id: Err("no value supplied for upload_id".to_string()),
}
}
}
impl Response {
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 file<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::WrittenFile>>,
T::Error: ::std::fmt::Display,
{
self.file = value
.try_into()
.map_err(|e| format!("error converting supplied value for file: {e}"));
self
}
pub fn sha256<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ExpectedSha256>,
T::Error: ::std::fmt::Display,
{
self.sha256 = value
.try_into()
.map_err(|e| format!("error converting supplied value for sha256: {e}"));
self
}
pub fn size_bytes<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ExpectedSizeBytes>,
T::Error: ::std::fmt::Display,
{
self.size_bytes = value
.try_into()
.map_err(|e| format!("error converting supplied value for size_bytes: {e}"));
self
}
pub fn staged_until<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ExpiresAt>>,
T::Error: ::std::fmt::Display,
{
self.staged_until = value
.try_into()
.map_err(|e| format!("error converting supplied value for staged_until: {e}"));
self
}
pub fn target<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::UploadTarget>,
T::Error: ::std::fmt::Display,
{
self.target = value
.try_into()
.map_err(|e| format!("error converting supplied value for target: {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<Response> for super::Response {
type Error = super::error::ConversionError;
fn try_from(value: Response) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
ext: value.ext?,
file: value.file?,
sha256: value.sha256?,
size_bytes: value.size_bytes?,
staged_until: value.staged_until?,
target: value.target?,
upload_id: value.upload_id?,
})
}
}
impl ::std::convert::From<super::Response> for Response {
fn from(value: super::Response) -> Self {
Self {
ext: Ok(value.ext),
file: Ok(value.file),
sha256: Ok(value.sha256),
size_bytes: Ok(value.size_bytes),
staged_until: Ok(value.staged_until),
target: Ok(value.target),
upload_id: Ok(value.upload_id),
}
}
}
#[derive(Clone, Debug)]
pub struct UploadTarget {
if_match:
::std::result::Result<::std::option::Option<super::WebsiteEtag>, ::std::string::String>,
kind: ::std::result::Result<super::UploadTargetKind, ::std::string::String>,
path:
::std::result::Result<::std::option::Option<super::WebsitePath>, ::std::string::String>,
}
impl ::std::default::Default for UploadTarget {
fn default() -> Self {
Self {
if_match: Ok(Default::default()),
kind: Err("no value supplied for kind".to_string()),
path: Ok(Default::default()),
}
}
}
impl UploadTarget {
pub fn if_match<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::WebsiteEtag>>,
T::Error: ::std::fmt::Display,
{
self.if_match = value
.try_into()
.map_err(|e| format!("error converting supplied value for if_match: {e}"));
self
}
pub fn kind<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::UploadTargetKind>,
T::Error: ::std::fmt::Display,
{
self.kind = value
.try_into()
.map_err(|e| format!("error converting supplied value for kind: {e}"));
self
}
pub fn path<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::WebsitePath>>,
T::Error: ::std::fmt::Display,
{
self.path = value
.try_into()
.map_err(|e| format!("error converting supplied value for path: {e}"));
self
}
}
impl ::std::convert::TryFrom<UploadTarget> for super::UploadTarget {
type Error = super::error::ConversionError;
fn try_from(
value: UploadTarget,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
if_match: value.if_match?,
kind: value.kind?,
path: value.path?,
})
}
}
impl ::std::convert::From<super::UploadTarget> for UploadTarget {
fn from(value: super::UploadTarget) -> Self {
Self {
if_match: Ok(value.if_match),
kind: Ok(value.kind),
path: Ok(value.path),
}
}
}
#[derive(Clone, Debug)]
pub struct WrittenFile {
etag: ::std::result::Result<super::WebsiteEtag, ::std::string::String>,
path: ::std::result::Result<super::WebsitePath, ::std::string::String>,
size_bytes: ::std::result::Result<::std::num::NonZeroU64, ::std::string::String>,
}
impl ::std::default::Default for WrittenFile {
fn default() -> Self {
Self {
etag: Err("no value supplied for etag".to_string()),
path: Err("no value supplied for path".to_string()),
size_bytes: Err("no value supplied for size_bytes".to_string()),
}
}
}
impl WrittenFile {
pub fn etag<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::WebsiteEtag>,
T::Error: ::std::fmt::Display,
{
self.etag = value
.try_into()
.map_err(|e| format!("error converting supplied value for etag: {e}"));
self
}
pub fn path<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::WebsitePath>,
T::Error: ::std::fmt::Display,
{
self.path = value
.try_into()
.map_err(|e| format!("error converting supplied value for path: {e}"));
self
}
pub fn size_bytes<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::num::NonZeroU64>,
T::Error: ::std::fmt::Display,
{
self.size_bytes = value
.try_into()
.map_err(|e| format!("error converting supplied value for size_bytes: {e}"));
self
}
}
impl ::std::convert::TryFrom<WrittenFile> for super::WrittenFile {
type Error = super::error::ConversionError;
fn try_from(
value: WrittenFile,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
etag: value.etag?,
path: value.path?,
size_bytes: value.size_bytes?,
})
}
}
impl ::std::convert::From<super::WrittenFile> for WrittenFile {
fn from(value: super::WrittenFile) -> Self {
Self {
etag: Ok(value.etag),
path: Ok(value.path),
size_bytes: Ok(value.size_bytes),
}
}
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/vtc/website/upload/commit/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 \"ExpiresAt\": {\n \"description\": \"After which the bundle is collected: staged bytes discarded, tokens and chunk requests refused. Short by design. For a chunked transfer a recipient may move it later as chunks are exchanged, never past its own ceiling — each chunk response reports the current value.\",\n \"format\": \"date-time\",\n \"title\": \"ExpiresAt\",\n \"type\": \"string\"\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 \"description\": \"Exactly one of `file` (for a `file` target) and `stagedUntil` (for a `bundle` target) is present — a rule over two members that this schema does not express.\",\n \"properties\": {\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"file\": {\n \"$ref\": \"#/$defs/WrittenFile\"\n },\n \"sha256\": {\n \"$ref\": \"#/$defs/ExpectedSha256\"\n },\n \"sizeBytes\": {\n \"$ref\": \"#/$defs/ExpectedSizeBytes\"\n },\n \"stagedUntil\": {\n \"$ref\": \"#/$defs/ExpiresAt\"\n },\n \"target\": {\n \"$ref\": \"#/$defs/UploadTarget\"\n },\n \"uploadId\": {\n \"$ref\": \"#/$defs/UploadId\"\n }\n },\n \"required\": [\n \"uploadId\",\n \"target\",\n \"sha256\",\n \"sizeBytes\"\n ],\n \"title\": \"VTC Website — Upload — Commit — 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 \"UploadTarget\": {\n \"additionalProperties\": false,\n \"description\": \"What the uploaded bytes are for, committed before any byte moves. `file`: one file at `path`, written into the site when the upload commits — the single-file write. `bundle`: a gzip-compressed tar of the whole site, staged when the upload commits and published by vtc/website/deploy. `path` is required for a `file` target and forbidden for a `bundle`, and `ifMatch` is allowed only for a `file` — rules over two members that this schema does not express; a community refuses a violation with `malformedRequest`. `ifMatch` is optimistic concurrency: the write commits only if the file's current content hash is this value.\",\n \"properties\": {\n \"ifMatch\": {\n \"$ref\": \"#/$defs/WebsiteEtag\"\n },\n \"kind\": {\n \"enum\": [\n \"file\",\n \"bundle\"\n ],\n \"type\": \"string\"\n },\n \"path\": {\n \"$ref\": \"#/$defs/WebsitePath\"\n }\n },\n \"required\": [\n \"kind\"\n ],\n \"title\": \"UploadTarget\",\n \"type\": \"object\"\n },\n \"WebsiteEtag\": {\n \"description\": \"A file's content hash: the lowercase hex SHA-256 of its bytes — the `etag` vtc/website/files/list reports, and the `ETag` a direct read of the path returns, without the HTTP quotes.\",\n \"pattern\": \"^[0-9a-f]{64}$\",\n \"title\": \"WebsiteEtag\",\n \"type\": \"string\"\n },\n \"WebsitePath\": {\n \"description\": \"A file's path relative to the site root, `/`-separated, with or without a leading `/`. The community resolves it inside the site root and refuses one that escapes the root (`..`), names a hidden file or directory (a segment beginning `.`), carries a blocklisted extension, or contains a control character — the same rules the public read handler applies, so nothing can be written that the site would refuse to serve, and nothing can be read that it would not.\",\n \"maxLength\": 1024,\n \"minLength\": 1,\n \"pattern\": \"^[^\\\\\\\\]+$\",\n \"title\": \"WebsitePath\",\n \"type\": \"string\"\n },\n \"WrittenFile\": {\n \"additionalProperties\": false,\n \"description\": \"The file a `file` upload wrote.\",\n \"properties\": {\n \"etag\": {\n \"$ref\": \"#/$defs/WebsiteEtag\"\n },\n \"path\": {\n \"$ref\": \"#/$defs/WebsitePath\"\n },\n \"sizeBytes\": {\n \"description\": \"The file's size in bytes.\",\n \"minimum\": 1,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"path\",\n \"etag\",\n \"sizeBytes\"\n ],\n \"title\": \"WrittenFile\",\n \"type\": \"object\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/vtc/website/upload/commit/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"description\": \"Complete an 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 — Upload — Commit — payload\",\n \"type\": \"object\"\n}\n",
);
}
impl crate::Payload for Response {
const TYPE_URI: &'static str =
"https://trusttasks.org/spec/vtc/website/upload/commit/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 \"ExpiresAt\": {\n \"description\": \"After which the bundle is collected: staged bytes discarded, tokens and chunk requests refused. Short by design. For a chunked transfer a recipient may move it later as chunks are exchanged, never past its own ceiling — each chunk response reports the current value.\",\n \"format\": \"date-time\",\n \"title\": \"ExpiresAt\",\n \"type\": \"string\"\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 \"description\": \"Exactly one of `file` (for a `file` target) and `stagedUntil` (for a `bundle` target) is present — a rule over two members that this schema does not express.\",\n \"properties\": {\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"file\": {\n \"$ref\": \"#/$defs/WrittenFile\"\n },\n \"sha256\": {\n \"$ref\": \"#/$defs/ExpectedSha256\"\n },\n \"sizeBytes\": {\n \"$ref\": \"#/$defs/ExpectedSizeBytes\"\n },\n \"stagedUntil\": {\n \"$ref\": \"#/$defs/ExpiresAt\"\n },\n \"target\": {\n \"$ref\": \"#/$defs/UploadTarget\"\n },\n \"uploadId\": {\n \"$ref\": \"#/$defs/UploadId\"\n }\n },\n \"required\": [\n \"uploadId\",\n \"target\",\n \"sha256\",\n \"sizeBytes\"\n ],\n \"title\": \"VTC Website — Upload — Commit — 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 \"UploadTarget\": {\n \"additionalProperties\": false,\n \"description\": \"What the uploaded bytes are for, committed before any byte moves. `file`: one file at `path`, written into the site when the upload commits — the single-file write. `bundle`: a gzip-compressed tar of the whole site, staged when the upload commits and published by vtc/website/deploy. `path` is required for a `file` target and forbidden for a `bundle`, and `ifMatch` is allowed only for a `file` — rules over two members that this schema does not express; a community refuses a violation with `malformedRequest`. `ifMatch` is optimistic concurrency: the write commits only if the file's current content hash is this value.\",\n \"properties\": {\n \"ifMatch\": {\n \"$ref\": \"#/$defs/WebsiteEtag\"\n },\n \"kind\": {\n \"enum\": [\n \"file\",\n \"bundle\"\n ],\n \"type\": \"string\"\n },\n \"path\": {\n \"$ref\": \"#/$defs/WebsitePath\"\n }\n },\n \"required\": [\n \"kind\"\n ],\n \"title\": \"UploadTarget\",\n \"type\": \"object\"\n },\n \"WebsiteEtag\": {\n \"description\": \"A file's content hash: the lowercase hex SHA-256 of its bytes — the `etag` vtc/website/files/list reports, and the `ETag` a direct read of the path returns, without the HTTP quotes.\",\n \"pattern\": \"^[0-9a-f]{64}$\",\n \"title\": \"WebsiteEtag\",\n \"type\": \"string\"\n },\n \"WebsitePath\": {\n \"description\": \"A file's path relative to the site root, `/`-separated, with or without a leading `/`. The community resolves it inside the site root and refuses one that escapes the root (`..`), names a hidden file or directory (a segment beginning `.`), carries a blocklisted extension, or contains a control character — the same rules the public read handler applies, so nothing can be written that the site would refuse to serve, and nothing can be read that it would not.\",\n \"maxLength\": 1024,\n \"minLength\": 1,\n \"pattern\": \"^[^\\\\\\\\]+$\",\n \"title\": \"WebsitePath\",\n \"type\": \"string\"\n },\n \"WrittenFile\": {\n \"additionalProperties\": false,\n \"description\": \"The file a `file` upload wrote.\",\n \"properties\": {\n \"etag\": {\n \"$ref\": \"#/$defs/WebsiteEtag\"\n },\n \"path\": {\n \"$ref\": \"#/$defs/WebsitePath\"\n },\n \"sizeBytes\": {\n \"description\": \"The file's size in bytes.\",\n \"minimum\": 1,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"path\",\n \"etag\",\n \"sizeBytes\"\n ],\n \"title\": \"WrittenFile\",\n \"type\": \"object\"\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::INCOMPLETE,
error_codes::DIGEST_MISMATCH,
error_codes::PRECONDITION_FAILED,
error_codes::PATH_REFUSED,
error_codes::SINGLE_FILE_WRITES_DISABLED,
];
/// 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/upload/commit:notFound`
///
/// No open upload with this id exists that the caller may commit.
///
/// Declared `retryable: false`.
pub const NOT_FOUND: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:notFound",
retryable: false,
};
/// `vtc/website/upload/commit:incomplete`
///
/// Chunks are missing. `details.remainingCount` says how many.
///
/// Declared `retryable: true`.
pub const INCOMPLETE: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:incomplete",
retryable: true,
};
/// `vtc/website/upload/commit:digestMismatch`
///
/// Every chunk verified, but the reassembled content does not hash to the SHA-256 committed at begin — the chunks are right and the order or the commitment is wrong. Nothing is written; the upload is discarded.
///
/// Declared `retryable: false`.
pub const DIGEST_MISMATCH: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:digestMismatch",
retryable: false,
};
/// `vtc/website/upload/commit:preconditionFailed`
///
/// The target file's current content hash is not the `ifMatch` committed at begin — somebody changed it since it was read. Nothing is written; read it again and start a new upload.
///
/// Declared `retryable: false`.
pub const PRECONDITION_FAILED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:preconditionFailed",
retryable: false,
};
/// `vtc/website/upload/commit:pathRefused`
///
/// The target path is refused by the site's path rules, re-checked at commit because the configuration may have changed since begin.
///
/// Declared `retryable: false`.
pub const PATH_REFUSED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:pathRefused",
retryable: false,
};
/// `vtc/website/upload/commit:singleFileWritesDisabled`
///
/// The site switched to managed deploy mode since the upload began; a file target can no longer be written.
///
/// Declared `retryable: false`.
pub const SINGLE_FILE_WRITES_DISABLED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "vtc/website/upload/commit:singleFileWritesDisabled",
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-000000000006\",\n \"type\": \"https://trusttasks.org/spec/vtc/website/upload/commit/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-000000000007\",\n \"type\": \"https://trusttasks.org/spec/vtc/website/upload/commit/0.1#response\",\n \"issuer\": \"did:example:community\",\n \"recipient\": \"did:example:administrator\",\n \"issuedAt\": \"2026-09-28T10:06:00Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000001ff\",\n \"payload\": {\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\",\n \"target\": {\n \"kind\": \"file\",\n \"path\": \"/events/index.html\",\n \"ifMatch\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\"\n },\n \"sha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"sizeBytes\": 300000,\n \"file\": {\n \"path\": \"/events/index.html\",\n \"etag\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"sizeBytes\": 300000\n }\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");
}
#[test]
fn response_example_2() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-000000000008\",\n \"type\": \"https://trusttasks.org/spec/vtc/website/upload/commit/0.1#response\",\n \"issuer\": \"did:example:community\",\n \"recipient\": \"did:example:administrator\",\n \"issuedAt\": \"2026-09-28T10:06:01Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000001fe\",\n \"payload\": {\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\",\n \"target\": {\n \"kind\": \"bundle\"\n },\n \"sha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"sizeBytes\": 300000,\n \"stagedUntil\": \"2026-09-28T11:06:00Z\"\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)] = &[
("Missing uploadId.", "{}"),
(
"A caller-supplied digest — the commitment was made at begin and cannot be restated here.",
"{\n \"expectedSha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\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)] = &[
(
"A written file with a non-hex etag.",
"{\n \"file\": {\n \"etag\": \"\\\"abc\\\"\",\n \"path\": \"/a.html\",\n \"sizeBytes\": 10\n },\n \"sha256\": \"3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a55\",\n \"sizeBytes\": 10,\n \"target\": {\n \"kind\": \"file\",\n \"path\": \"/a.html\"\n },\n \"uploadId\": \"8c7b6a59-4837-4261-9f0e-1d2c3b4a5968\"\n}",
),
(
"Response without the verified hash.",
"{\n \"sizeBytes\": 10,\n \"stagedUntil\": \"2026-09-28T11:06:00Z\",\n \"target\": {\n \"kind\": \"bundle\"\n },\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::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
);
}
}
}