Portable OpenRPC documents with exact JSON and source-preserving syntax
///|
test {
let source = " {\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"API\",\"version\":\"service-9\"},\"methods\":[],\"x-number\":9007199254740993} "
let result = @openrpc.read(source)
assert_eq(result.cst().unwrap().reproduce(), source)
let document = result.document().unwrap()
assert_eq(document.info().version(), "service-9")
assert_eq(
@openrpc.write(document).unwrap(),
"{\"info\":{\"title\":\"API\",\"version\":\"service-9\"},\"methods\":[],\"openrpc\":\"1.4.0\",\"x-number\":9007199254740993}",
)
}///|
test {
let cst = @exact.parse("{\"n\":9007199254740993}", source_id=Some("api.json")).unwrap()
let location = cst.source_map().locations(["n"])[0]
assert_eq(location.source_id(), Some("api.json"))
assert_eq(cst.slice(location.value_span()).unwrap(), "9007199254740993")
assert_eq(cst.slice(location.key_span().unwrap()).unwrap(), "\"n\"")
}///|
test {
let source = "{\"x-n\": 1.0, \"methods\": [{\"name\": \"b\", \"params\": []}, {\"name\": \"a\", \"params\": [{\"$ref\": \"#/components/contentDescriptors/P\"}]}],\r\n \"info\": {\"version\": \"1\", \"title\": \"API\"}, \"openrpc\": \"1.4.0\", \"components\": {\"contentDescriptors\": {\"P\": {\"name\": \"p\", \"schema\": true}}}}"
let read = @openrpc.read(source)
let document = read.document().unwrap()
assert_eq(read.cst().unwrap().reproduce(), source)
let compact = @openrpc.write(document).unwrap()
assert_eq(
compact,
"{\"components\":{\"contentDescriptors\":{\"P\":{\"name\":\"p\",\"schema\":true}}},\"info\":{\"title\":\"API\",\"version\":\"1\"},\"methods\":[{\"name\":\"b\",\"params\":[]},{\"name\":\"a\",\"params\":[{\"$ref\":\"#/components/contentDescriptors/P\"}]}],\"openrpc\":\"1.4.0\",\"x-n\":1.0}",
)
let pretty = @openrpc.write(document, format=@exact.Pretty).unwrap()
assert_true(pretty.has_prefix("{\n \"components\": {"))
// Both formats read back to the same document.
let again = @openrpc.read(pretty).document().unwrap()
assert_eq(@openrpc.write(again).unwrap(), compact)
}///|
test {
let source = "{\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"API\",\"version\":\"1\"},\r\n \"methods\": [],\r\n \"x-owner\": \"team-a\"}"
let cst = @openrpc.read(source).cst().unwrap()
let node = cst.node_at(["x-owner"]).unwrap()
let edited = @openrpc.replace_value(cst, node, @exact.Value::string("team-b")).unwrap()
let new_cst = edited.cst().unwrap()
assert_eq(new_cst.reproduce(), source.replace(old="team-a", new="team-b"))
// The old handle is stale for the edited text.
assert_true(new_cst.replace_value(node, @exact.Value::null()) is Err(_))
// Validation of the edited document locates findings in the new text.
let title = new_cst.node_at(["info", "title"]).unwrap()
let broken = @openrpc.replace_value(new_cst, title, @exact.Value::null()).unwrap()
assert_true(broken.document() is None)
let span = broken.diagnostics()[0].span().unwrap()
assert_eq(broken.cst().unwrap().slice(span), Some("null"))
}///|
test {
let exact = @exact.parse("9007199254740993").unwrap().lower().unwrap()
let core = @exact.to_core_json(exact).unwrap()
assert_true(@exact.from_core_json(core).unwrap().equals(exact).unwrap())
let code = @openrpc.ErrorCode::parse("-90071992547409931234567890").unwrap()
assert_eq(code.number().token(), "-90071992547409931234567890")
let extensions = @openrpc.Extensions::from_json(
@exact.parse("{\"x-value\":false}").unwrap().lower().unwrap(),
).unwrap()
assert_true(
extensions.get(@openrpc.ExtensionName::parse("x-value").unwrap()) is Some(_),
)
}| Facet | Checks |
|---|---|
| model | Objects reached by validation decode into the typed model |
| schemas | Embedded draft-07 schema documents, including regex syntax |
| references | Every reference resolves within the supplied resources |
| normative | Rules from the pinned prose of the document's version |
| artifact | The document's own JSON against its version's pinned metaschema |
| Discrepancy | Prose | Artifact |
|---|---|---|
| OptionalParameterBeforeRequired | Optional params come last | No rule |
| ServerNameOmitted | 1.3.2 requires a server name | Optional |
| LicenseNameOmitted | 1.3.2 requires a license name | Optional |
///|
test {
let document = @openrpc.read(
"{\"openrpc\":\"1.3.2\",\"info\":{\"title\":\"API\",\"version\":\"1\"},\"methods\":[{\"name\":\"find\",\"params\":[{\"name\":\"limit\",\"required\":false,\"schema\":true},{\"name\":\"query\",\"required\":true,\"schema\":true}]}]}",
)
.document()
.unwrap()
// The prose forbids an optional parameter before a required one.
let strict = @openrpc.validate_closed(document)
assert_eq(strict.report().normative(), @openrpc.Failed)
assert_true(strict.document() is None)
// Compatibility reports the same finding as a listed warning.
let compatible = @openrpc.validate_closed(
document,
profile=@openrpc.MetaschemaCompatibility,
)
let order = compatible.report().findings()[0]
assert_eq(order.severity(), @openrpc.Warning)
assert_eq(order.discrepancy(), Some(@openrpc.OptionalParameterBeforeRequired))
let validated = compatible.document().unwrap()
assert_eq(validated.profile(), @openrpc.MetaschemaCompatibility)
// Parameter order is unchanged.
assert_eq(
@openrpc.write(validated.document()).unwrap(),
@openrpc.write(document).unwrap(),
)
}///|
test {
let source = "{\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"😀\",\"version\":\"1\"},\n\"methods\":[{\"name\":\"a\",\"params\":[]},{\"$ref\":\"#/x-copy\"}],\"x-copy\":{\"name\":\"a\",\"params\":[]}}"
let read = @openrpc.read(source, source_id=Some("api.json"))
let source_map = read.source_map().unwrap()
let report = @openrpc.validate(read.document().unwrap(), source_map=Some(source_map))
let duplicate = report.findings()[0]
assert_eq(duplicate.diagnostic().code(), @exact.DuplicateName)
assert_eq(duplicate.location().path(), ["x-copy", "name"])
assert_eq(duplicate.chain()[0].path(), ["methods", "1", "$ref"])
assert_eq(duplicate.related()[0].path(), ["methods", "0", "name"])
// The emoji counts as two UTF-16 units; lines and columns start at 1.
let span = duplicate.location().span().unwrap()
let position = source_map.position(span.start()).unwrap()
assert_eq((position.line(), position.column()), (2, 75))
}///|
test {
let document = @openrpc.read(
"{\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"API\",\"version\":\"1\"},\"methods\":[{\"name\":\"a\",\"params\":[]},{\"name\":\"a\",\"params\":[]},{\"name\":\"a\",\"params\":[]}]}",
)
.document()
.unwrap()
let limits = @exact.Limits::new(max_findings=1)
let report = @openrpc.validate(document, limits~)
assert_eq(report.normative(), @openrpc.Failed)
assert_true(
report
.findings()
.any(f => f.diagnostic().code() == @exact.LimitExceeded),
)
assert_true(@openrpc.validate_closed(document, limits~).document() is None)
}///|
test {
let document = @openrpc.read(
"{\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"API\",\"version\":\"1\"},\"methods\":[{\"name\":\"log\",\"params\":[],\"result\":{\"name\":\"done\",\"schema\":{\"type\":\"null\"}},\"examples\":[{\"name\":\"e\",\"params\":[],\"result\":{\"name\":\"r\",\"value\":42}}]}]}",
)
.document()
.unwrap()
// The example value 42 does not match the null schema, and that is not a
// document finding.
let result = @openrpc.validate_closed(document)
assert_true(result.document() is Some(_))
assert_true(result.report().findings().is_empty())
}///|
test {
let document = @openrpc.read(
"{\"openrpc\":\"1.4.0\",\"info\":{\"title\":\"API\",\"version\":\"1\"},\"methods\":[{\"$ref\":\"shared.json#/methods/ping\"}]}",
)
.document()
.unwrap()
let base = @reference.UriReference::parse("https://api.example/openrpc.json").unwrap()
// Without the shared resource the reference is unresolved, not failed.
let open = @openrpc.validate(document, base=Some(base))
assert_eq(open.references(), @openrpc.Incomplete)
assert_true(
@openrpc.validate_closed(document, base=Some(base)).document() is None,
)
// Supplying it, under an alias, closes the check. Nothing is fetched.
let shared = @reference.Resource::new(
@reference.UriReference::parse("https://cdn.example/v1/shared.json").unwrap(),
@exact.parse(
"{\"methods\":{\"ping\":{\"name\":\"ping\",\"params\":[],\"result\":{\"name\":\"pong\",\"schema\":{\"$ref\":\"#/definitions/Pong\"}}}},\"definitions\":{\"Pong\":{\"type\":\"string\"}}}",
)
.unwrap()
.lower()
.unwrap(),
aliases=[
@reference.UriReference::parse("https://api.example/shared.json").unwrap(),
],
schema_roots=[@reference.JsonPointer::parse("/definitions/Pong").unwrap()],
).unwrap()
let resources = @reference.ResourceSet::new([shared]).unwrap()
let closed = @openrpc.validate_closed(document, resources~, base=Some(base))
assert_eq(closed.report().references(), @openrpc.Passed)
assert_true(closed.document() is Some(_))
}///|
test {
let null_schema = @schema.Schema::from_keywords([
@schema.Type(@schema.TypeConstraint::one(@schema.NullType)),
]).unwrap()
let result = @openrpc.ContentDescriptor::new(
"result",
null_schema,
required=Some(true),
).unwrap()
let op = @openrpc.Method::new(
@openrpc.MethodName::parse("ping").unwrap(),
[],
result=Some(@openrpc.Inline(result)),
param_structure=Some(@openrpc.ByName),
).unwrap()
let document = @openrpc.Document::new(
@openrpc.DocumentVersion::parse("1.3.2").unwrap(),
@openrpc.Info::new("Example API", "service-v9").unwrap(),
[@openrpc.Inline(op)],
).unwrap()
assert_eq(document.info().version(), "service-v9")
assert_true(@openrpc.write(document).unwrap().contains("\"type\":\"null\""))
let notification = @openrpc.Method::new(
@openrpc.MethodName::parse("notify").unwrap(),
[],
).unwrap()
assert_true(notification.result() is None)
}///|
test {
let schema = @schema.Schema::from_keywords([
@schema.Type(@schema.TypeConstraint::one(@schema.StringType)),
]).unwrap()
let components = @openrpc.Components::new(
schemas=Some([(@openrpc.ComponentKey::parse("Text").unwrap(), schema)]),
).unwrap()
let descriptor = @openrpc.ContentDescriptor::new(
"text",
@schema.Schema::from_json(
@exact.parse("{\"$ref\":\"#/components/schemas/Text\"}")
.unwrap()
.lower()
.unwrap(),
).unwrap(),
).unwrap()
assert_eq(components.schemas().unwrap()[0].0.to_string(), "Text")
assert_true(descriptor.schema().kind() is @schema.ReferenceSchema(_))
let name = @openrpc.ExtensionName::parse("x-contract").unwrap()
let extensions = @openrpc.Extensions::from_entries([(name, schema.to_json())]).unwrap()
let submitted = @schema.Schema::from_json(extensions.get(name).unwrap()).unwrap()
assert_true(
@schema.validate(submitted, root=submitted.to_json())
.diagnostics()
.is_empty(),
)
}///|
test {
let error = @openrpc.ErrorObject::new(
@openrpc.ErrorCode::parse("-90071992547409931234567890").unwrap(),
"Failed",
data=Some(
@exact.parse("{\"$ref\":\"literal\",\"schema\":17}")
.unwrap()
.lower()
.unwrap(),
),
).unwrap()
let external = @openrpc.Example::new(
@openrpc.ExternalExample(
@reference.UriReference::parse("https://example.invalid/value").unwrap(),
),
).unwrap()
let components = @openrpc.Components::new(
errors=Some([(@openrpc.ComponentKey::parse("E").unwrap(), error)]),
examples=Some([
(@openrpc.ComponentKey::parse("External").unwrap(), external),
]),
example_pairings=Some([]),
example_pairing_objects=Some([]),
).unwrap()
assert_true(components.to_json().get("examplePairings") is Some(_))
assert_true(components.to_json().get("examplePairingObjects") is Some(_))
assert_true(components.errors().unwrap()[0].1.data() is Some(_))
}pub struct ClosedValidation {
report : ValidationReport
document : ValidatedDocument?
} derive(Debug)pub struct Components {
value : Value
schemas : Array[(ComponentKey, Schema)]?
content_descriptors : Array[(ComponentKey, ContentDescriptor)]?
examples : Array[(ComponentKey, Example)]?
links : Array[(ComponentKey, Link)]?
errors : Array[(ComponentKey, ErrorObject)]?
tags : Array[(ComponentKey, Tag)]?
example_pairings : Array[(ComponentKey, ExamplePairing)]?
example_pairing_objects : Array[(ComponentKey, ExamplePairing)]?
} derive(Debug)fn Components::new(schemas? : Array[(ComponentKey, Schema)]?, content_descriptors? : Array[(ComponentKey, ContentDescriptor)]?, examples? : Array[(ComponentKey, Example)]?, links? : Array[(ComponentKey, Link)]?, errors? : Array[(ComponentKey, ErrorObject)]?, tags? : Array[(ComponentKey, Tag)]?, example_pairings? : Array[(ComponentKey, ExamplePairing)]?, example_pairing_objects? : Array[(ComponentKey, ExamplePairing)]?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Components, Diagnostic]fn Contact::new(name? : String?, url? : UriReference?, email? : String?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Contact, Diagnostic]fn ContentDescriptor::from_json(value : Value, limits? : Limits) -> Result[ContentDescriptor, Diagnostic]fn ContentDescriptor::new(name : String, schema : Schema, summary? : String?, description? : String?, required? : Bool?, deprecated? : Bool?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[ContentDescriptor, Diagnostic]pub struct Document {
version : DocumentVersion
info : Info
methods : Array[InlineOrReference[Method]]
value : Value
schema_uri : UriReference?
servers : Array[Server]?
components : Components?
external_docs : ExternalDocumentation?
} derive(Debug)fn Document::as_resource(self : Document, uri : UriReference, aliases? : Array[UriReference], limits? : Limits) -> Result[Resource, Diagnostic]fn Document::new(version : DocumentVersion, info : Info, methods : Array[InlineOrReference[Method]], schema? : UriReference?, servers? : Array[Server]?, components? : Components?, external_docs? : ExternalDocumentation?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Document, Array[Diagnostic]]fn ErrorObject::new(code : ErrorCode, message : String, data? : Value?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[ErrorObject, Diagnostic]fn Example::new(payload : ExampleValue, name? : String?, summary? : String?, description? : String?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Example, Diagnostic]fn ExamplePairing::new(name : String, params : Array[InlineOrReference[Example]], summary? : String?, description? : String?, result? : InlineOrReference[Example]?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[ExamplePairing, Diagnostic]pub(all) enum ExampleValue {
NoExampleValue
LiteralExample(Value)
ExternalExample(UriReference)
} derive(Debug)fn ExternalDocumentation::from_json(value : Value, limits? : Limits) -> Result[ExternalDocumentation, Diagnostic]fn ExternalDocumentation::new(url : UriReference, description? : String?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[ExternalDocumentation, Diagnostic]pub struct Finding {
diagnostic : Diagnostic
severity : Severity
phase : Phase
// private fields
} derive(Debug)fn Info::new(title : String, version : String, description? : String?, terms_of_service? : UriReference?, contact? : Contact?, license? : License?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Info, Diagnostic]fn License::new(name? : String?, url? : UriReference?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[License, Diagnostic]fn Link::new(name? : String?, summary? : String?, description? : String?, target_method? : MethodName?, params? : Array[(String, Value)]?, server? : Server?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Link, Diagnostic]pub struct Method {
name : MethodName
params : Array[InlineOrReference[ContentDescriptor]]
result : InlineOrReference[ContentDescriptor]?
param_structure : ParamStructure?
value : Value
} derive(Debug)fn Method::new(name : MethodName, params : Array[InlineOrReference[ContentDescriptor]], result? : InlineOrReference[ContentDescriptor]?, tags? : Array[InlineOrReference[Tag]]?, summary? : String?, description? : String?, external_docs? : ExternalDocumentation?, deprecated? : Bool?, servers? : Array[Server]?, errors? : Array[InlineOrReference[ErrorObject]]?, links? : Array[InlineOrReference[Link]]?, param_structure? : ParamStructure?, examples? : Array[InlineOrReference[ExamplePairing]]?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Method, Diagnostic]pub(all) enum ObjectKind {
DocumentObject
InfoObject
ContactObject
LicenseObject
ExternalDocumentationObject
ServerVariableObject
ServerObject
MethodObject
ContentDescriptorObject
TagObject
ErrorObjectKind
ExampleObject
ExamplePairingObject
LinkObject
ComponentsObject
ReferenceObject
} derive(Eq, Debug)pub struct ReadResult {
cst : CstDocument?
document : Document?
diagnostics : Array[Diagnostic]
} derive(Debug)fn Server::new(url : String, name? : String?, summary? : String?, description? : String?, variables? : Array[(String, ServerVariable)]?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Server, Diagnostic]fn ServerVariable::new(default_value : String, enum_values? : Array[String]?, description? : String?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[ServerVariable, Diagnostic]fn Tag::new(name : String, summary? : String?, description? : String?, external_docs? : ExternalDocumentation?, extensions? : Extensions, additional_fields? : Array[(String, Value)], limits? : Limits) -> Result[Tag, Diagnostic]pub struct ValidatedDocument {
document : Document
report : ValidationReport
// private fields
} derive(Debug)pub struct ValidationReport {
profile : ValidationProfile
version : DocumentVersion
findings : Array[Finding]
model : Coverage
schemas : Coverage
references : Coverage
normative : Coverage
artifact : Coverage
} derive(Debug)fn replace_value(cst : CstDocument, node : NodeId, value : Value, limits? : Limits) -> Result[ReadResult, Diagnostic]fn validate(document : Document, profile? : ValidationProfile, resources? : ResourceSet, base? : UriReference?, source_map? : SourceMap?, limits? : Limits) -> ValidationReportfn validate_closed(document : Document, profile? : ValidationProfile, resources? : ResourceSet, base? : UriReference?, source_map? : SourceMap?, limits? : Limits) -> ClosedValidationInstall
Download zipPortable OpenRPC documents with exact JSON and source-preserving syntax