///|
/// A decoded document of a versioned bundle. It keeps the exact release that selected its root.
pub struct VersionedValue {
priv release : @contract.Release
priv profile : @contract.ProfileId
priv value : CodecValue
} derive(Eq, Debug)
///|
pub fn VersionedValue::release(self : VersionedValue) -> @contract.Release {
self.release
}
///|
/// The profile that read the document.
pub fn VersionedValue::profile(self : VersionedValue) -> @contract.ProfileId {
self.profile
}
///|
pub fn VersionedValue::value(self : VersionedValue) -> CodecValue {
self.value
}
///|
priv struct PreparedBinding {
binding : @contract.ReleaseBinding
codecs : Array[(@contract.ProfileId, PreparedCodec)]
}
///|
/// A prepared versioned document bundle. It selects one root and profile from the document's version
/// before it decodes the payload, and it never converts a document to another release.
pub struct PreparedBundle {
priv declaration : @contract.BundleDeclaration
priv bindings : Array[PreparedBinding]
priv adapters : AdapterRegistry
priv limits : CodecLimits
}
///|
pub fn PreparedBundle::prepare(
model : @smithy.Model,
bundle : @contract.BundleId,
adapters~ : AdapterRegistry = AdapterRegistry::new(),
limits~ : CodecLimits = CodecLimits::default(),
) -> PreparedBundle raise @contract.ContractError {
let declarations = @contract.declared_bundles(model)
guard declarations.get(bundle) is Some(declaration) else {
raise @contract.UnknownBundle(bundle)
}
guard adapters.get(declaration.version_adapter) is Some(version) else {
raise @contract.MissingAdapter(
declaration.declared_on,
declaration.version_adapter,
)
}
guard version.signature.output == StringKind else {
raise @contract.InvalidDeclaration(
declaration.declared_on,
"Version adapter \{declaration.version_adapter} must produce release text.",
)
}
let bindings : Array[PreparedBinding] = []
for binding in declaration.releases {
let codecs : Array[(@contract.ProfileId, PreparedCodec)] = []
for profile in binding.profiles {
let codec = PreparedCodec::prepare(
model,
binding.root,
profile=Some(profile),
adapters~,
limits~,
)
let field = codec.contract.shapes.get(binding.root).unwrap().members.search_by(m => {
m.wire == declaration.version_field
},
)
guard field is Some(_) else {
raise @contract.InvalidDeclaration(
binding.root,
"The root has no member with the version field \{declaration.version_field}.",
)
}
codecs.push((profile, codec))
}
bindings.push({ binding, codecs, })
}
{ declaration, bindings, adapters, limits, }
}
///|
pub fn PreparedBundle::declaration(
self : PreparedBundle,
) -> @contract.BundleDeclaration {
self.declaration
}
///|
/// The codec of `release` under `profile`, or under the release's default profile.
pub fn PreparedBundle::codec(
self : PreparedBundle,
release : @contract.Release,
profile~ : @contract.ProfileId? = None,
) -> PreparedCodec? {
for prepared in self.bindings {
if prepared.binding.interval.contains(release) {
for entry in prepared.codecs {
if profile is None || profile == Some(entry.0) {
return Some(entry.1)
}
}
}
}
None
}
///|
/// Read the version of a parsed document and select its codec. Findings go to the request.
fn PreparedBundle::dispatch(
self : PreparedBundle,
request : Request,
json : Json,
profile : @contract.ProfileId?,
) -> (@contract.Release, @contract.ProfileId, PreparedCodec)? {
let root = @validation.InstancePath::root()
let anchor = self.declaration.declared_on
let field = self.declaration.version_field.to_string()
let at = field_path(root, field)
guard json is Object(fields) else {
request.emit(KindMismatch, root, anchor, expected(ObjectKind, json))
return None
}
guard fields.get(field) is Some(version) else {
request.emit(
MissingVersion,
at,
anchor,
"I expected the version field \{field}, but found it absent.",
)
return None
}
let adapter = self.adapters.get(self.declaration.version_adapter).unwrap()
if !adapter.signature.inputs.contains(@contract.JsonKind::of(version)) {
request.emit(
InvalidVersion,
at,
anchor,
"I expected a version, but found \{@contract.JsonKind::of(version)}.",
)
return None
}
if !request.charge(at, anchor) {
return None
}
let read = (adapter.reader)(version, { meter: request.meter, })
if request.meter.exhausted() {
request.stop(at, anchor, "work")
return None
}
let release = match read {
Ok(Text(text)) =>
@contract.Release::parse(text) catch {
_ => {
request.emit(
InvalidVersion,
at,
anchor,
"Version adapter \{adapter.id()} produced \{text.escape()}, which is not a release.",
)
return None
}
}
Ok(_) => {
request.emit(InvalidVersion, at, anchor, "I expected release text.")
return None
}
Err(failure) => {
request.emit(
InvalidVersion,
at.append(failure.path),
anchor,
failure.message,
)
return None
}
}
guard self.bindings.search_by(b => b.binding.interval.contains(release))
is Some(i) else {
let supported = self.bindings
.map(b => b.binding.interval.to_string())
.join(", ")
request.emit(
UnsupportedVersion,
at,
anchor,
"Release \{release} is outside the supported releases \{supported} of bundle \{self.declaration.id}.",
)
return None
}
let prepared = self.bindings[i]
match profile {
None => {
let (id, codec) = prepared.codecs[0]
Some((release, id, codec))
}
Some(wanted) =>
match prepared.codecs.search_by(entry => entry.0 == wanted) {
Some(j) => Some((release, wanted, prepared.codecs[j].1))
None => {
request.emit(
VersionConflict,
at,
anchor,
"The document declares release \{release}, which profile \{wanted} does not read.",
)
None
}
}
}
}
///|
fn PreparedBundle::read_text(
self : PreparedBundle,
request : Request,
text : String,
profile : @contract.ProfileId?,
) -> VersionedValue? {
guard parse_text(request, text, self.declaration.declared_on) is Some(json) else {
return None
}
guard self.dispatch(request, json, profile) is Some((release, id, codec)) else {
return None
}
match codec.read_parsed(request, json) {
Some(value) =>
Some({ release, profile: id, value: { root: codec.root, value, }, })
None => None
}
}
///|
/// Check a versioned document. An explicit profile must read the document's release.
pub fn PreparedBundle::check(
self : PreparedBundle,
text : String,
profile~ : @contract.ProfileId? = None,
) -> CodecResult {
let request = Request::new(self.limits)
ignore(self.read_text(request, text, profile))
request.result()
}
///|
pub fn PreparedBundle::decode(
self : PreparedBundle,
text : String,
profile~ : @contract.ProfileId? = None,
) -> VersionedValue raise CodecFailure {
let request = Request::new(self.limits)
match self.read_text(request, text, profile) {
Some(value) => value
None => raise CodecFailure(request.result())
}
}
///|
fn PreparedBundle::encode_in(
self : PreparedBundle,
request : Request,
value : VersionedValue,
profile : @contract.ProfileId?,
) -> String? {
let root = @validation.InstancePath::root()
let anchor = self.declaration.declared_on
let profile = match profile {
Some(p) => p
None => value.profile
}
guard self.codec(value.release, profile=Some(profile)) is Some(codec) else {
request.emit(
VersionConflict,
root,
anchor,
"Profile \{profile} does not write release \{value.release} of bundle \{self.declaration.id}.",
)
return None
}
// The version member of the value must name the same release.
let field = codec.contract.shapes.get(codec.contract.root).unwrap().members
.search_by(m => m.wire == self.declaration.version_field)
.unwrap()
let name = codec.contract.shapes.get(codec.contract.root).unwrap().members[field].name
match value.value.value.get(name) {
Some(String(text)) =>
if !same_release(text, value.release) {
request.emit(
VersionConflict,
field_path(root, name),
anchor,
"The value's version \{text.escape()} differs from its release \{value.release}.",
)
return None
}
_ => {
request.emit(
VersionConflict,
field_path(root, name),
anchor,
"I expected the value to hold its release text.",
)
return None
}
}
codec.encode_in(request, value.value)
}
///|
/// Encode a versioned value with the canonical spelling of its own release. A profile that does not write
/// that release is refused; no value is converted to another release.
pub fn PreparedBundle::encode(
self : PreparedBundle,
value : VersionedValue,
profile~ : @contract.ProfileId? = None,
) -> String raise CodecFailure {
let request = Request::new(self.limits)
match self.encode_in(request, value, profile) {
Some(text) => text
None => raise CodecFailure(request.result())
}
}
///|
/// Decode with the document's release and re-encode with `profile`, or the reading profile. One request
/// budget covers both.
pub fn PreparedBundle::canonicalize(
self : PreparedBundle,
text : String,
profile~ : @contract.ProfileId? = None,
output~ : @contract.ProfileId? = None,
) -> String raise CodecFailure {
let request = Request::new(self.limits)
guard self.read_text(request, text, profile) is Some(value) else {
raise CodecFailure(request.result())
}
match self.encode_in(request, value, output) {
Some(out) => out
None => raise CodecFailure(request.result())
}
}
///|
fn same_release(text : String, release : @contract.Release) -> Bool {
(@contract.Release::parse(text) catch { _ => return false }) == release
}