///|
/// A reusable codec of one model root under one profile. Checking, decoding and encoding use the same
/// detached wire contract, the same adapters and the same Smithy value constraints.
pub struct PreparedCodec {
priv contract : @contract.WireContract
priv validator : @validation.PreparedValidator
priv adapters : AdapterRegistry
priv limits : CodecLimits
priv root : SemanticRoot
}
///|
/// Prepare a codec. Without a profile, the base rules and the standard layouts apply. Preparation fails
/// on an unknown profile, a missing adapter or an inconsistent or unsupported declaration; it never falls
/// back to another layout.
pub fn PreparedCodec::prepare(
model : @smithy.Model,
root : @smithy.ShapeId,
profile~ : @contract.ProfileId? = None,
adapters~ : AdapterRegistry = AdapterRegistry::new(),
limits~ : CodecLimits = CodecLimits::default(),
) -> PreparedCodec raise @contract.ContractError {
let contract = @contract.WireContract::prepare(
model,
root,
profile~,
adapters=adapters.signatures(),
)
let config = @validation.Config::new(
limits=@validation.Limits::new(
depth=limits.depth,
work=limits.work,
findings=limits.findings,
) catch {
@validation.ConfigurationError(message) =>
raise @contract.InvalidTarget(root, message)
},
)
let validator = @validation.PreparedValidator::prepare(model, root, config~) catch {
@validation.InvalidTarget(id, message) =>
raise @contract.InvalidTarget(id, message)
@validation.InvalidConfiguration(message) =>
raise @contract.InvalidTarget(root, message)
@validation.InvalidModel(id, message) =>
raise @contract.InvalidDeclaration(id, message)
@validation.UnsupportedConstraint(id, message) =>
raise @contract.UnsupportedDeclaration(id, message)
}
{
contract,
validator,
adapters,
limits,
root: { shape: root, fingerprint: contract.fingerprint, },
}
}
///|
/// The detached wire contract, for generators and schema exporters.
pub fn PreparedCodec::contract(self : PreparedCodec) -> @contract.WireContract {
self.contract
}
///|
pub fn PreparedCodec::semantic_root(self : PreparedCodec) -> SemanticRoot {
self.root
}
///|
pub fn PreparedCodec::limits(self : PreparedCodec) -> CodecLimits {
self.limits
}
///|
fn utf8_bytes(text : String) -> Int32 {
let mut bytes = 0
for c in text.iter() {
let n = c.to_int()
bytes += if n < 0x80 {
1
} else if n < 0x800 {
2
} else if n < 0x10000 {
3
} else {
4
}
}
bytes
}
///|
/// Parse bounded, duplicate-aware JSON text. Findings go to the request.
fn parse_text(
request : Request,
text : String,
shape : @smithy.ShapeId,
) -> Json? {
let root = @validation.InstancePath::root()
let bytes = utf8_bytes(text)
if bytes > request.limits.input_bytes {
request.stop(root, shape, "input size")
return None
}
// Parsing charges one unit for each 64 bytes of input.
if !request.charge(root, shape, units=bytes / 64 + 1) {
return None
}
let parsed = @validation.parse_json_value(text) catch {
@validation.InvalidJson(offset, message) => {
request.emit(InvalidJson, root, shape, "At offset \{offset}: " + message)
return None
}
@validation.DuplicateJsonKey(offset) => {
request.emit(
DuplicateName,
root,
shape,
"At offset \{offset}: I found a duplicate decoded object member name.",
)
return None
}
@validation.JsonInputLimit(offset, message) => {
request.stop(root, shape, "input nesting")
request.findings[request.findings.length() - 1] = {
..request.findings[request.findings.length() - 1],
message: "At offset \{offset}: " + message,
}
return None
}
}
Some(parsed)
}
///|
/// Check a projected node against the Smithy value constraints, sharing the request's work meter.
fn PreparedCodec::constrain(
self : PreparedCodec,
request : Request,
node : LNode,
) -> Unit {
let result = self.validator.validate_metered(node, meter=request.meter)
self.absorb(request, result)
}
///|
fn PreparedCodec::absorb(
self : PreparedCodec,
request : Request,
result : @validation.ValidationResult,
) -> Unit {
for finding in result.findings {
if finding.code == LimitExceeded {
request.stop(finding.path, finding.shape, "work")
} else {
request.emit(
Constraint(finding.code),
finding.path,
finding.shape,
finding.message,
)
}
}
if !result.complete {
request.stop(@validation.InstancePath::root(), self.root.shape, "work")
}
}
///|
/// Read a parsed JSON value under this codec's contract, sharing `request`.
fn PreparedCodec::read_parsed(
self : PreparedCodec,
request : Request,
json : Json,
) -> LogicalValue? {
match
read_json(
self.contract,
self.adapters,
request,
json,
@validation.InstancePath::root(),
) {
Some(node) => {
self.constrain(request, node)
if request.complete && request.findings.is_empty() {
Some(node.to_logical())
} else {
None
}
}
None => None
}
}
///|
fn PreparedCodec::read_text(
self : PreparedCodec,
request : Request,
text : String,
) -> LogicalValue? {
match parse_text(request, text, self.root.shape) {
Some(json) => self.read_parsed(request, json)
None => None
}
}
///|
/// Check JSON text. A complete check without findings agrees with a successful decode.
pub fn PreparedCodec::check(self : PreparedCodec, text : String) -> CodecResult {
let request = Request::new(self.limits)
ignore(self.read_text(request, text))
request.result()
}
///|
/// Decode JSON text into a value of this codec's semantic root.
pub fn PreparedCodec::decode(
self : PreparedCodec,
text : String,
) -> CodecValue raise CodecFailure {
let request = Request::new(self.limits)
match self.read_text(request, text) {
Some(value) => { root: self.root, value, }
None => raise CodecFailure(request.result())
}
}
///|
/// Check a caller's logical value against this codec's root and return it as a codec value. This is the
/// validated builder: the value is copied into its normalized form.
pub fn PreparedCodec::value(
self : PreparedCodec,
value : LogicalValue,
) -> CodecValue raise CodecFailure {
let request = Request::new(self.limits)
match self.admit(request, value) {
Some(normalized) => { root: self.root, value: normalized, }
None => raise CodecFailure(request.result())
}
}
///|
fn PreparedCodec::admit(
self : PreparedCodec,
request : Request,
value : LogicalValue,
) -> LogicalValue? {
guard normalize(self.contract, request, value) is Some(normalized) else {
return None
}
let result = self.validator.validate_metered(normalized, meter=request.meter)
self.absorb(request, result)
if request.complete && request.findings.is_empty() {
Some(normalized)
} else {
None
}
}
///|
/// Encode a value of this codec's semantic root. The value is checked again before any output exists, and
/// a value of another root is refused.
pub fn PreparedCodec::encode(
self : PreparedCodec,
value : CodecValue,
) -> String raise CodecFailure {
let request = Request::new(self.limits)
match self.encode_in(request, value) {
Some(text) => text
None => raise CodecFailure(request.result())
}
}
///|
fn PreparedCodec::encode_in(
self : PreparedCodec,
request : Request,
value : CodecValue,
) -> String? {
if value.root != self.root {
request.emit(
IncompatibleValue,
@validation.InstancePath::root(),
self.root.shape,
"I expected a value of \{self.root}, but found a value of \{value.root}. Convert it explicitly.",
)
return None
}
guard self.admit(request, value.value) is Some(normalized) else {
return None
}
guard write_value(self.contract, self.adapters, request, normalized)
is Some(text) else {
return None
}
if utf8_bytes(text) > self.limits.output_bytes {
request.stop(
@validation.InstancePath::root(),
self.root.shape,
"output size",
)
return None
}
Some(text)
}
///|
/// Decode and re-encode JSON text in this codec's canonical form. One request budget covers both.
pub fn PreparedCodec::canonicalize(
self : PreparedCodec,
text : String,
) -> String raise CodecFailure {
let request = Request::new(self.limits)
guard self.read_text(request, text) is Some(value) else {
raise CodecFailure(request.result())
}
match self.encode_in(request, { root: self.root, value, }) {
Some(out) => out
None => raise CodecFailure(request.result())
}
}