///|
/// A document (deserialized JSON) with a concrete interpretation under a
/// specification.
///
/// In other words, a `Json` value along with a `Specification` which describes
/// how the document interacts with referencing -- both internally (how it
/// refers to other resources) and externally (how it should be identified
/// such that it is referenceable by other documents).
pub struct Resource {
contents : Json
specification : Specification
}
///|
/// Create a resource with an explicit specification
/// (Python's `Resource(contents=..., specification=...)`).
pub fn Resource::new(
contents : Json,
specification~ : Specification,
) -> Resource {
{ contents, specification, }
}
///|
/// Create a resource guessing which specification applies to the contents.
///
/// Without `default_specification`, raises `CannotDetermineSpecification` if
/// the contents have no discernible information (a `$schema` keyword) which
/// could be used to guess which specification they identify as (and
/// `UnknownDialect` for unknown `$schema`s). See `Specification::detect`.
pub fn Resource::from_contents(
contents : Json,
default_specification? : Specification,
) -> Resource raise ReferencingError {
let specification = Specification::detect(
contents,
default?=default_specification,
)
specification.create_resource(contents)
}
///|
/// Create an opaque `Resource` -- i.e. one with the opaque specification.
pub fn Resource::new_opaque(contents : Json) -> Resource {
opaque_specification.create_resource(contents)
}
///|
/// Retrieve this resource's (specification-specific) identifier, with any
/// trailing `#` characters stripped.
pub fn Resource::id(self : Resource) -> String? {
match self.specification.id_of(self.contents) {
None => None
Some(id) => Some(rstrip_hash(id))
}
}
///|
/// Retrieve this resource's subresources. Each is interpreted by detecting its
/// specification, defaulting to this resource's specification.
pub fn Resource::subresources(self : Resource) -> Array[Resource] {
self.specification
.subresources_of(self.contents)
.map(each => {
let specification = Specification::detect(each, default=self.specification) catch {
_ => self.specification
}
specification.create_resource(each)
})
}
///|
/// Retrieve this resource's (specification-specific) anchors.
pub fn Resource::anchors(self : Resource) -> Array[Anchor] {
self.specification.anchors_in(self.contents)
}
///|
/// Resolve the given JSON pointer (the fragment of a reference, e.g.
/// `/definitions/foo`) within this resource.
///
/// As upstream, the pointer (minus its first character) is first
/// percent-decoded with `@urllib.unquote`, then split on `/`; each segment
/// indexing into an object has `~1` replaced by `/` and then `~0` by `~`,
/// while segments indexing into arrays (and strings) are parsed as Python
/// integers (so negative indices count from the end, as in Python).
///
/// Raises `PointerToNowhere` if the pointer points to a location not present
/// in the document.
pub fn Resource::pointer(
self : Resource,
pointer : String,
resolver : Resolver,
) -> Resolved raise {
if pointer == "" {
return { contents: self.contents, resolver, }
}
let mut contents = self.contents
let mut resolver = resolver
let mut segments : Array[Segment] = []
let decoded = @urllib.unquote(pointer.view(start_offset=1).to_owned())
for raw in decoded.split("/") {
let raw = raw.to_owned()
let (segment, next) = match contents {
Array(items) => {
guard py_int(raw) is Some(index) else {
raise PointerToNowhere(reference=pointer, resource=self)
}
let len = items.length()
let i = if index < 0 { index + len } else { index }
guard i >= 0 && i < len else {
raise PointerToNowhere(reference=pointer, resource=self)
}
(Index(index), items[i])
}
String(s) => {
// Python strings are sequences of code points, too.
guard py_int(raw) is Some(index) else {
raise PointerToNowhere(reference=pointer, resource=self)
}
let chars = s.iter().to_array()
let len = chars.length()
let i = if index < 0 { index + len } else { index }
guard i >= 0 && i < len else {
raise PointerToNowhere(reference=pointer, resource=self)
}
(Index(index), Json::string(chars[i].to_string()))
}
Object(obj) => {
let key = raw
.replace_all(old="~1", new="/")
.replace_all(old="~0", new="~")
guard obj.get(key) is Some(value) else {
raise PointerToNowhere(reference=pointer, resource=self)
}
(Key(key), value)
}
_ => raise PointerToNowhere(reference=pointer, resource=self)
}
contents = next
segments.push(segment)
let last = resolver
resolver = self.specification.maybe_in_subresource(
segments,
resolver,
self.specification.create_resource(contents),
)
if !physical_equal(resolver, last) {
segments = []
}
}
{ contents, resolver, }
}
///|
/// Resources are equal when their contents are equal and their specifications
/// are equal.
pub impl Eq for Resource with fn equal(self, other) {
self.contents == other.contents && self.specification == other.specification
}
///|
/// `Resource(contents=..., _specification=)`, as
/// Python's `repr`.
pub impl Show for Resource with fn output(self, logger) {
logger.write_string(
"Resource(contents=\{py_repr(self.contents)}, _specification=\{self.specification})",
)
}
///|
/// Python's `str.rstrip("#")`.
fn rstrip_hash(s : String) -> String {
let mut end = s.length()
while end > 0 && s[end - 1] == '#' {
end -= 1
}
s.view(end_offset=end).to_owned()
}