///|
/// Text ingest for local Object Contract fixtures.
///|
pub(all) suberror ContractIngestError {
ParseFailed(String)
MissingField(String)
InvalidField(String, String)
} derive(Debug)
///|
pub fn parse_contract_registry_text(
text : String,
) -> ContractRegistry raise ContractIngestError {
let trimmed = text[:].trim()
if trimmed.has_prefix("{") {
parse_contract_registry_json(text)
} else {
parse_contract_registry_yaml(text)
}
}
///|
pub fn parse_contract_registry_json(
text : String,
) -> ContractRegistry raise ContractIngestError {
let parsed = @json.parse(text[:]) catch {
err => raise ContractIngestError::ParseFailed(err.to_string())
}
guard parsed is { "objects": Array(objects_json), .. } else {
raise ContractIngestError::MissingField("objects")
}
let registry = ContractRegistry::new()
for object_json in objects_json {
registry.register(parse_contract_object_json(object_json))
}
registry
}
///|
pub fn parse_contract_registry_yaml(
text : String,
) -> ContractRegistry raise ContractIngestError {
let registry = ContractRegistry::new()
for block in parse_key_value_blocks(text) {
registry.register(parse_contract_object_fields(block))
}
registry
}
///|
fn parse_contract_object_json(
json : Json,
) -> ObjectContract raise ContractIngestError {
let id = json_required_string(json, "id")
let object = ObjectContract::make(
id~,
kind=parse_object_kind(json_required_string(json, "kind"), "kind"),
schema=parse_schema(json_required_string(json, "schema"), "schema"),
timing={
period_ns: json_optional_int64(json, "period_ns"),
max_age_ns: json_optional_int64(json, "max_age_ns"),
deadline_ns: json_optional_int64(json, "deadline_ns"),
valid_for_ns: json_optional_int64(json, "valid_for_ns"),
clock_requirement: parse_clock_requirement(
json_optional_string(json, "clock_requirement"),
"clock_requirement",
),
},
safety={
authority: parse_authority(json_optional_string(json, "authority")),
min_payload_digest: json_optional_int(json, "min_payload_digest"),
max_payload_digest: json_optional_int(json, "max_payload_digest"),
timeout_action: json_optional_string(json, "timeout_action").unwrap_or(
"hold_last",
),
},
)
let mut out = object
match json {
{ "bindings": Array(bindings_json), .. } =>
for binding_json in bindings_json {
out = out.with_binding(parse_binding_json(id, binding_json))
}
_ => ()
}
out
}
///|
fn parse_binding_json(
object_id : String,
json : Json,
) -> ObjectBinding raise ContractIngestError {
{
object_id: json_optional_string(json, "object_id").unwrap_or(object_id),
binding_id: json_required_string(json, "binding_id"),
medium_id: json_optional_string(json, "medium_id").unwrap_or(""),
label: json_required_string(json, "label"),
direction: parse_direction(
json_optional_string(json, "direction"),
"direction",
),
kind: parse_binding_kind(json_optional_string(json, "kind")),
index: json_optional_uint(json, "index"),
subindex: json_optional_byte(json, "subindex"),
channel: json_optional_string(json, "channel").unwrap_or(""),
key: json_optional_string(json, "key").unwrap_or(""),
authority: parse_authority(json_optional_string(json, "authority")),
}
}
///|
fn parse_contract_object_fields(
fields : Array[(String, String)],
) -> ObjectContract raise ContractIngestError {
let id = required_field(fields, "object")
let object = ObjectContract::make(
id~,
kind=parse_object_kind(required_field(fields, "kind"), "kind"),
schema=parse_schema(required_field(fields, "schema"), "schema"),
timing={
period_ns: optional_int64_field(fields, "period_ns"),
max_age_ns: optional_int64_field(fields, "max_age_ns"),
deadline_ns: optional_int64_field(fields, "deadline_ns"),
valid_for_ns: optional_int64_field(fields, "valid_for_ns"),
clock_requirement: parse_clock_requirement(
optional_field(fields, "clock_requirement"),
"clock_requirement",
),
},
safety={
authority: parse_authority(optional_field(fields, "authority")),
min_payload_digest: optional_int_field(fields, "min_payload_digest"),
max_payload_digest: optional_int_field(fields, "max_payload_digest"),
timeout_action: optional_field(fields, "timeout_action").unwrap_or(
"hold_last",
),
},
)
let mut out = object
for field in fields {
let (key, value) = field
if key == "binding" {
out = out.with_binding(parse_binding_row(id, value))
}
}
out
}
///|
fn parse_binding_row(
object_id : String,
value : String,
) -> ObjectBinding raise ContractIngestError {
let parts = value.split(",").to_array()
guard parts.length() >= 2 else {
raise ContractIngestError::InvalidField("binding", value)
}
let binding_id = clean_value(parts[0].trim().to_owned())
let label = clean_value(parts[1].trim().to_owned())
let medium_id = if parts.length() > 2 {
clean_value(parts[2].trim().to_owned())
} else {
""
}
let direction = if parts.length() > 3 {
parse_direction(Some(clean_value(parts[3].trim().to_owned())), "binding")
} else {
None
}
let authority = if parts.length() > 4 {
parse_authority(Some(clean_value(parts[4].trim().to_owned())))
} else {
Any
}
let kind = if parts.length() > 5 {
parse_binding_kind(Some(clean_value(parts[5].trim().to_owned())))
} else {
TraceLabel
}
let binding_object_id = binding_named_option(parts, "object_id").unwrap_or(
if parts.length() > 6 &&
!clean_value(parts[6].trim().to_owned()).contains("=") {
clean_value(parts[6].trim().to_owned())
} else {
object_id
},
)
let index = match binding_named_option(parts, "index") {
Some(raw) => Some(parse_text_uint(raw, "index"))
None => None
}
let subindex = match binding_named_option(parts, "subindex") {
Some(raw) => Some(parse_text_byte(raw, "subindex"))
None => None
}
let channel = binding_named_option(parts, "channel").unwrap_or("")
let key = binding_named_option(parts, "key").unwrap_or("")
{
object_id: binding_object_id,
binding_id,
medium_id,
label,
direction,
kind,
index,
subindex,
channel,
key,
authority,
}
}
///|
fn binding_named_option(parts : Array[StringView], name : String) -> String? {
let mut found : String? = None
for i in 6.. ObjectKind raise ContractIngestError {
match normalized(value) {
"signal" => Signal
"command" => Command
"query" => Query
"event" => Event
"lifecycle" => Lifecycle
other => raise ContractIngestError::InvalidField(field, other)
}
}
///|
fn parse_schema(
value : String,
field : String,
) -> Schema raise ContractIngestError {
match normalized(value) {
"bool" => Bool
"i8" => I8
"i32" => I32
"u16" => U16
"state" => State
"text" => Text
other => raise ContractIngestError::InvalidField(field, other)
}
}
///|
fn parse_authority(value : String?) -> Authority {
match value {
None => Any
Some(raw) =>
match normalized(raw) {
"" | "any" => Any
"read_only" | "readonly" | "read-only" => ReadOnly
other => Role(other)
}
}
}
///|
fn parse_clock_requirement(
value : String?,
field : String,
) -> ObjectClockRequirement raise ContractIngestError {
match value {
None => AnyClock
Some(raw) =>
match normalized(raw) {
"any" | "unspecified" => AnyClock
"physical" | "physical-clock" | "physical_clock" => PhysicalClock
other => raise ContractIngestError::InvalidField(field, other)
}
}
}
///|
fn parse_binding_kind(value : String?) -> BindingKind raise ContractIngestError {
match value {
None => TraceLabel
Some(raw) =>
match normalized(raw) {
"" | "trace_label" | "trace-label" | "trace" => TraceLabel
"ethercat_pdo" | "ethercat-pdo" => EtherCatPdo
"coe_sdo" | "coe-sdo" => CoeSdo
"canopen_sdo" | "canopen-sdo" => CanopenSdo
"canopen_pdo" | "canopen-pdo" => CanopenPdo
"zenoh_key" | "zenoh-key" => ZenohKey
"modbus_register" | "modbus-register" => ModbusRegister
other => raise ContractIngestError::InvalidField("kind", other)
}
}
}
///|
fn parse_direction(
value : String?,
field : String,
) -> @trace.TraceDirection? raise ContractIngestError {
match value {
None => None
Some(raw) =>
match normalized(raw) {
"" | "any" => None
"tx" => Some(@trace.Tx)
"rx" => Some(@trace.Rx)
"fault" => Some(@trace.Fault)
"tick" => Some(@trace.Tick)
other => raise ContractIngestError::InvalidField(field, other)
}
}
}
///|
fn parse_key_value_blocks(
text : String,
) -> Array[Array[(String, String)]] raise ContractIngestError {
let blocks : Array[Array[(String, String)]] = []
let mut current : Array[(String, String)] = []
for raw_line in text.split("\n") {
let line = raw_line.trim().to_owned()
if line == "" || line.has_prefix("#") {
()
} else if line == "---" {
if !current.is_empty() {
blocks.push(current)
current = []
}
} else {
current.push(parse_key_value_line(line))
}
}
if !current.is_empty() {
blocks.push(current)
}
blocks
}
///|
fn parse_key_value_line(
line : String,
) -> (String, String) raise ContractIngestError {
let parts = if line.contains(":") {
line.split(":").to_array()
} else {
line.split("=").to_array()
}
guard parts.length() == 2 else {
raise ContractIngestError::ParseFailed("invalid line: " + line)
}
(
normalized(parts[0].trim().to_owned()),
clean_value(parts[1].trim().to_owned()),
)
}
///|
fn required_field(
fields : Array[(String, String)],
name : String,
) -> String raise ContractIngestError {
match optional_field(fields, name) {
Some(value) => value
None => raise ContractIngestError::MissingField(name)
}
}
///|
fn optional_field(fields : Array[(String, String)], name : String) -> String? {
let mut found : String? = None
for field in fields {
let (key, value) = field
if key == name {
found = Some(value)
}
}
found
}
///|
fn optional_int_field(
fields : Array[(String, String)],
name : String,
) -> Int? raise ContractIngestError {
match optional_field(fields, name) {
Some(value) => Some(parse_text_int(value, name))
None => None
}
}
///|
fn optional_int64_field(
fields : Array[(String, String)],
name : String,
) -> Int64? raise ContractIngestError {
match optional_int_field(fields, name) {
Some(value) => Some(Int64::from_int(value))
None => None
}
}
///|
fn json_required_string(
json : Json,
field : String,
) -> String raise ContractIngestError {
match json_optional_string(json, field) {
Some(value) => value
None => raise ContractIngestError::MissingField(field)
}
}
///|
fn json_optional_string(
json : Json,
field : String,
) -> String? raise ContractIngestError {
match (field, json) {
("id", { "id": value, .. }) => Some(decode_json_string(value, field))
("kind", { "kind": value, .. }) => Some(decode_json_string(value, field))
("schema", { "schema": value, .. }) =>
Some(decode_json_string(value, field))
("clock_requirement", { "clock_requirement": value, .. }) =>
Some(decode_json_string(value, field))
("authority", { "authority": value, .. }) =>
Some(decode_json_string(value, field))
("timeout_action", { "timeout_action": value, .. }) =>
Some(decode_json_string(value, field))
("binding_id", { "binding_id": value, .. }) =>
Some(decode_json_string(value, field))
("object_id", { "object_id": value, .. }) =>
Some(decode_json_string(value, field))
("medium_id", { "medium_id": value, .. }) =>
Some(decode_json_string(value, field))
("label", { "label": value, .. }) => Some(decode_json_string(value, field))
("direction", { "direction": value, .. }) =>
Some(decode_json_string(value, field))
("channel", { "channel": value, .. }) =>
Some(decode_json_string(value, field))
("key", { "key": value, .. }) => Some(decode_json_string(value, field))
_ => None
}
}
///|
fn json_optional_int(
json : Json,
field : String,
) -> Int? raise ContractIngestError {
match (field, json) {
("min_payload_digest", { "min_payload_digest": value, .. }) =>
Some(decode_json_int(value, field))
("max_payload_digest", { "max_payload_digest": value, .. }) =>
Some(decode_json_int(value, field))
_ => None
}
}
///|
fn json_optional_int64(
json : Json,
field : String,
) -> Int64? raise ContractIngestError {
match (field, json) {
("period_ns", { "period_ns": value, .. }) =>
Some(decode_json_int64(value, field))
("max_age_ns", { "max_age_ns": value, .. }) =>
Some(decode_json_int64(value, field))
("deadline_ns", { "deadline_ns": value, .. }) =>
Some(decode_json_int64(value, field))
("valid_for_ns", { "valid_for_ns": value, .. }) =>
Some(decode_json_int64(value, field))
_ => None
}
}
///|
fn json_optional_uint(
json : Json,
field : String,
) -> UInt? raise ContractIngestError {
match (field, json) {
("index", { "index": value, .. }) => Some(decode_json_uint(value, field))
_ => None
}
}
///|
fn json_optional_byte(
json : Json,
field : String,
) -> Byte? raise ContractIngestError {
match (field, json) {
("subindex", { "subindex": value, .. }) =>
Some(decode_json_byte(value, field))
_ => None
}
}
///|
fn decode_json_string(
json : Json,
field : String,
) -> String raise ContractIngestError {
@json.from_json(json) catch {
_ => raise ContractIngestError::InvalidField(field, json.stringify())
}
}
///|
fn decode_json_int(
json : Json,
field : String,
) -> Int raise ContractIngestError {
@json.from_json(json) catch {
_ => raise ContractIngestError::InvalidField(field, json.stringify())
}
}
///|
fn decode_json_int64(
json : Json,
field : String,
) -> Int64 raise ContractIngestError {
Int64::from_int(decode_json_int(json, field))
}
///|
fn decode_json_uint(
json : Json,
field : String,
) -> UInt raise ContractIngestError {
let value = decode_json_int(json, field)
if value < 0 {
raise ContractIngestError::InvalidField(field, value.to_string())
}
value.reinterpret_as_uint()
}
///|
fn decode_json_byte(
json : Json,
field : String,
) -> Byte raise ContractIngestError {
let value = decode_json_int(json, field)
if value < 0 || value > 255 {
raise ContractIngestError::InvalidField(field, value.to_string())
}
value.to_byte()
}
///|
fn parse_text_int(
value : String,
field : String,
) -> Int raise ContractIngestError {
let body = clean_value(value)
@string.parse_int(body[:]) catch {
_ => raise ContractIngestError::InvalidField(field, body)
}
}
///|
fn parse_text_uint(
value : String,
field : String,
) -> UInt raise ContractIngestError {
let parsed = parse_text_int(value, field)
if parsed < 0 {
raise ContractIngestError::InvalidField(field, parsed.to_string())
}
parsed.reinterpret_as_uint()
}
///|
fn parse_text_byte(
value : String,
field : String,
) -> Byte raise ContractIngestError {
let parsed = parse_text_int(value, field)
if parsed < 0 || parsed > 255 {
raise ContractIngestError::InvalidField(field, parsed.to_string())
}
parsed.to_byte()
}
///|
fn clean_value(value : String) -> String {
let trimmed = value[:].trim().to_owned()
let without_comma = if trimmed.has_suffix(",") {
trimmed[:trimmed.length() - 1].to_owned()
} else {
trimmed
}
if without_comma.length() >= 2 &&
(
(without_comma.has_prefix("\"") && without_comma.has_suffix("\"")) ||
(without_comma.has_prefix("'") && without_comma.has_suffix("'"))
) {
without_comma[1:without_comma.length() - 1].to_owned()
} else {
without_comma
}
}
///|
fn normalized(value : String) -> String {
clean_value(value).to_lower()
}