use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct InvalidComponentRef {
pub(crate) reference: String,
}
impl fmt::Display for InvalidComponentRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid reference {:?}: expected JSON pointer starting with #/",
self.reference
)
}
}
impl std::error::Error for InvalidComponentRef {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct EndpointPath {
path: String,
base_len: usize,
}
impl EndpointPath {
pub(crate) fn for_path(api_path: &str) -> Self {
let escaped = escape_json_pointer_segment(api_path);
let path = format!("#/paths/{}", escaped);
Self {
base_len: path.len(),
path,
}
}
pub(crate) fn for_operation(api_path: &str, method: &str) -> Self {
let escaped_path = escape_json_pointer_segment(api_path);
let path = format!("#/paths/{}/{}", escaped_path, method);
Self {
base_len: path.len(),
path,
}
}
pub(crate) fn append(&self, segment: &str) -> Self {
let escaped = escape_json_pointer_segment(segment);
Self {
path: format!("{}/{}", self.path, escaped),
base_len: self.base_len,
}
}
fn as_str(&self) -> &str {
&self.path
}
fn base_path(&self) -> &str {
&self.path[..self.base_len]
}
fn subpath(&self) -> &str {
let sub = &self.path[self.base_len..];
sub.strip_prefix('/').unwrap_or(sub)
}
fn without_subpath(&self) -> Self {
Self {
path: self.base_path().to_owned(),
base_len: self.base_len,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct RefTargetPath {
path: String,
base_len: usize,
}
impl RefTargetPath {
fn parse(pointer: &str) -> Option<Self> {
pointer.starts_with("#/").then(|| Self {
base_len: pointer.len(),
path: pointer.to_string(),
})
}
fn append(&self, segment: &str) -> Self {
let escaped = escape_json_pointer_segment(segment);
Self {
path: format!("{}/{}", self.path, escaped),
base_len: self.base_len,
}
}
fn as_str(&self) -> &str {
&self.path
}
fn base_path(&self) -> &str {
&self.path[..self.base_len]
}
fn subpath(&self) -> &str {
let sub = &self.path[self.base_len..];
sub.strip_prefix('/').unwrap_or(sub)
}
fn without_subpath(&self) -> Self {
Self {
path: self.base_path().to_owned(),
base_len: self.base_len,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum PathState {
PathsRoot,
AtEndpoint(EndpointPath),
AtComponent {
current: RefTargetPath,
origin_ref: EndpointPath,
intermediate_refs: Vec<RefTargetPath>,
},
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JsonPathStack {
state: PathState,
}
impl JsonPathStack {
pub(crate) fn for_endpoint(endpoint: EndpointPath) -> Self {
Self {
state: PathState::AtEndpoint(endpoint),
}
}
pub(crate) fn paths_root() -> Self {
Self {
state: PathState::PathsRoot,
}
}
#[cfg(test)]
fn has_root(&self) -> bool {
!matches!(self.state, PathState::PathsRoot)
}
#[cfg(test)]
fn is_at_endpoint(&self) -> bool {
matches!(self.state, PathState::AtEndpoint(_))
}
#[cfg(test)]
fn is_at_component(&self) -> bool {
matches!(self.state, PathState::AtComponent { .. })
}
pub fn current_pointer(&self) -> &str {
match &self.state {
PathState::PathsRoot => "#/paths",
PathState::AtEndpoint(path) => path.as_str(),
PathState::AtComponent { current, .. } => current.as_str(),
}
}
pub fn component_base(&self) -> Option<&str> {
match &self.state {
PathState::PathsRoot | PathState::AtEndpoint(_) => None,
PathState::AtComponent { current, .. } => Some(current.base_path()),
}
}
pub fn base_and_subpath(&self) -> (&str, &str) {
match &self.state {
PathState::PathsRoot => ("#/paths", ""),
PathState::AtEndpoint(path) => (path.base_path(), path.subpath()),
PathState::AtComponent { current, .. } => (current.base_path(), current.subpath()),
}
}
pub fn without_subpath(&self) -> Self {
let state = match &self.state {
PathState::PathsRoot => PathState::PathsRoot,
PathState::AtEndpoint(path) => PathState::AtEndpoint(path.without_subpath()),
PathState::AtComponent {
current,
origin_ref,
intermediate_refs,
} => PathState::AtComponent {
current: current.without_subpath(),
origin_ref: origin_ref.clone(),
intermediate_refs: intermediate_refs.clone(),
},
};
Self { state }
}
pub fn endpoint_base(&self) -> Option<&str> {
match &self.state {
PathState::PathsRoot => None,
PathState::AtEndpoint(path) => Some(path.base_path()),
PathState::AtComponent { origin_ref, .. } => Some(origin_ref.base_path()),
}
}
pub(crate) fn append(&self, segment: &str) -> JsonPathStack {
let state = match &self.state {
PathState::PathsRoot => {
panic!("cannot append to paths_root (use for_endpoint for traversal)")
}
PathState::AtEndpoint(path) => PathState::AtEndpoint(path.append(segment)),
PathState::AtComponent {
current,
origin_ref,
intermediate_refs,
} => PathState::AtComponent {
current: current.append(segment),
origin_ref: origin_ref.clone(),
intermediate_refs: intermediate_refs.clone(),
},
};
Self { state }
}
pub(crate) fn push(&self, reference: &str) -> Result<JsonPathStack, InvalidComponentRef> {
let schema = RefTargetPath::parse(reference).ok_or_else(|| InvalidComponentRef {
reference: reference.to_string(),
})?;
let state = match &self.state {
PathState::PathsRoot => {
panic!("cannot push from paths_root (no endpoint context)")
}
PathState::AtEndpoint(path) => PathState::AtComponent {
current: schema,
origin_ref: path.append("$ref"),
intermediate_refs: Vec::new(),
},
PathState::AtComponent {
current,
origin_ref,
intermediate_refs,
} => {
let mut new_intermediates = intermediate_refs.clone();
new_intermediates.push(current.append("$ref"));
PathState::AtComponent {
current: schema,
origin_ref: origin_ref.clone(),
intermediate_refs: new_intermediates,
}
}
};
Ok(Self { state })
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
match &self.state {
PathState::PathsRoot => {
Box::new(std::iter::once("#/paths")) as Box<dyn Iterator<Item = &str>>
}
PathState::AtEndpoint(path) => {
Box::new(std::iter::once(path.as_str())) as Box<dyn Iterator<Item = &str>>
}
PathState::AtComponent {
current,
origin_ref,
intermediate_refs,
} => Box::new(
std::iter::once(current.as_str())
.chain(intermediate_refs.iter().rev().map(RefTargetPath::as_str))
.chain(std::iter::once(origin_ref.as_str())),
),
}
}
}
impl fmt::Debug for JsonPathStack {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = f.debug_list();
for path in self.iter() {
out.entry(&path);
}
out.finish()
}
}
impl fmt::Display for JsonPathStack {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for path in self.iter() {
if !first {
write!(f, " -> ")?;
}
write!(f, "{}", path)?;
first = false;
}
Ok(())
}
}
fn escape_json_pointer_segment(segment: &str) -> String {
segment.replace('~', "~0").replace('/', "~1")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoint_path_for_path() {
let path = EndpointPath::for_path("/users");
assert_eq!(path.as_str(), "#/paths/~1users");
assert_eq!(path.base_path(), "#/paths/~1users");
assert_eq!(path.subpath(), "");
}
#[test]
fn endpoint_path_for_path_escapes() {
let path = EndpointPath::for_path("/users/{id}/posts");
assert_eq!(path.as_str(), "#/paths/~1users~1{id}~1posts");
}
#[test]
fn endpoint_path_for_operation() {
let path = EndpointPath::for_operation("/users", "get");
assert_eq!(path.as_str(), "#/paths/~1users/get");
assert_eq!(path.base_path(), "#/paths/~1users/get");
assert_eq!(path.subpath(), "");
}
#[test]
fn endpoint_path_append() {
let path = EndpointPath::for_operation("/users", "get").append("responses");
assert_eq!(path.as_str(), "#/paths/~1users/get/responses");
assert_eq!(path.base_path(), "#/paths/~1users/get");
assert_eq!(path.subpath(), "responses");
}
#[test]
fn endpoint_path_append_escapes() {
let path = EndpointPath::for_operation("/users", "get")
.append("foo/bar")
.append("a~b");
assert_eq!(path.as_str(), "#/paths/~1users/get/foo~1bar/a~0b");
}
#[test]
fn component_path_parse_valid() {
let path = RefTargetPath::parse("#/components/schemas/User");
assert!(path.is_some());
assert_eq!(path.unwrap().as_str(), "#/components/schemas/User");
}
#[test]
fn component_path_parse_valid_various() {
assert!(RefTargetPath::parse("#/components/responses/NotFound").is_some());
assert!(RefTargetPath::parse("#/components/parameters/PageSize").is_some());
assert!(RefTargetPath::parse("#/paths/~1users/get").is_some());
assert!(RefTargetPath::parse("#/definitions/User").is_some());
}
#[test]
fn component_path_parse_invalid() {
assert!(RefTargetPath::parse("components/schemas/User").is_none());
assert!(RefTargetPath::parse("/components/schemas/User").is_none());
assert!(RefTargetPath::parse("https://example.com/schema.json").is_none());
}
#[test]
fn component_path_append() {
let path = RefTargetPath::parse("#/components/schemas/User")
.unwrap()
.append("properties")
.append("name");
assert_eq!(path.as_str(), "#/components/schemas/User/properties/name");
}
#[test]
fn ref_target_path_base_and_subpath() {
let path = RefTargetPath::parse("#/components/schemas/User").unwrap();
assert_eq!(path.base_path(), "#/components/schemas/User");
assert_eq!(path.subpath(), "");
let appended = path.append("properties").append("name");
assert_eq!(appended.base_path(), "#/components/schemas/User");
assert_eq!(appended.subpath(), "properties/name");
}
#[test]
fn ref_target_path_without_subpath() {
let path = RefTargetPath::parse("#/components/schemas/User")
.unwrap()
.append("properties")
.append("name");
let base_only = path.without_subpath();
assert_eq!(base_only.as_str(), "#/components/schemas/User");
assert_eq!(base_only.base_path(), "#/components/schemas/User");
assert_eq!(base_only.subpath(), "");
}
#[test]
fn endpoint_path_without_subpath() {
let path = EndpointPath::for_operation("/users", "get")
.append("responses")
.append("200");
assert_eq!(path.subpath(), "responses/200");
let base_only = path.without_subpath();
assert_eq!(base_only.as_str(), "#/paths/~1users/get");
assert_eq!(base_only.base_path(), "#/paths/~1users/get");
assert_eq!(base_only.subpath(), "");
}
#[test]
fn json_path_stack_for_endpoint() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint);
assert_eq!(stack.current_pointer(), "#/paths/~1users/get");
assert!(stack.is_at_endpoint());
assert!(stack.has_root());
}
#[test]
fn json_path_stack_paths_root() {
let stack = JsonPathStack::paths_root();
assert_eq!(stack.current_pointer(), "#/paths");
assert!(!stack.has_root());
}
#[test]
fn json_path_stack_append() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.append("responses")
.append("200")
.append("schema");
assert_eq!(
stack.current_pointer(),
"#/paths/~1users/get/responses/200/schema"
);
assert!(stack.is_at_endpoint());
}
#[test]
fn json_path_stack_push() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.append("responses")
.append("200")
.append("schema")
.push("#/components/schemas/User")
.unwrap();
assert_eq!(stack.current_pointer(), "#/components/schemas/User");
assert!(stack.is_at_component());
let entries: Vec<_> = stack.iter().collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0], "#/components/schemas/User");
assert_eq!(entries[1], "#/paths/~1users/get/responses/200/schema/$ref");
}
#[test]
fn json_path_stack_iter_order() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.push("#/components/schemas/A")
.unwrap()
.push("#/components/schemas/B")
.unwrap();
let entries: Vec<_> = stack.iter().collect();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0], "#/components/schemas/B");
assert_eq!(entries[1], "#/components/schemas/A/$ref");
assert_eq!(entries[2], "#/paths/~1users/get/$ref");
}
#[test]
fn json_path_stack_display() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.push("#/components/schemas/User")
.unwrap();
let displayed = format!("{}", stack);
assert_eq!(
displayed,
"#/components/schemas/User -> #/paths/~1users/get/$ref"
);
}
#[test]
fn json_path_stack_push_invalid_ref_returns_error() {
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint);
let err = stack
.push("not/a/json/pointer")
.expect_err("expected error for invalid reference");
assert_eq!(err.reference, "not/a/json/pointer");
assert_eq!(
err.to_string(),
"invalid reference \"not/a/json/pointer\": \
expected JSON pointer starting with #/"
);
}
#[test]
fn json_path_stack_base_and_subpath() {
let stack = JsonPathStack::paths_root();
assert_eq!(stack.base_and_subpath(), ("#/paths", ""));
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint);
assert_eq!(stack.base_and_subpath(), ("#/paths/~1users/get", ""));
let stack = stack.append("responses").append("200");
assert_eq!(
stack.base_and_subpath(),
("#/paths/~1users/get", "responses/200")
);
let stack = stack
.append("schema")
.push("#/components/schemas/User")
.unwrap();
assert_eq!(stack.base_and_subpath(), ("#/components/schemas/User", ""));
let stack = stack.append("properties").append("name");
assert_eq!(
stack.base_and_subpath(),
("#/components/schemas/User", "properties/name")
);
}
#[test]
fn json_path_stack_without_subpath() {
let stack = JsonPathStack::paths_root();
assert_eq!(stack.without_subpath().current_pointer(), "#/paths");
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.append("responses")
.append("200");
let stripped = stack.without_subpath();
assert_eq!(stripped.current_pointer(), "#/paths/~1users/get");
assert_eq!(stripped.base_and_subpath(), ("#/paths/~1users/get", ""));
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint)
.push("#/components/schemas/User")
.unwrap()
.append("properties")
.append("name");
let stripped = stack.without_subpath();
assert_eq!(stripped.current_pointer(), "#/components/schemas/User");
assert_eq!(
stripped.base_and_subpath(),
("#/components/schemas/User", "")
);
let entries: Vec<_> = stripped.iter().collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0], "#/components/schemas/User");
assert_eq!(entries[1], "#/paths/~1users/get/$ref");
}
#[test]
fn json_path_stack_component_base() {
assert_eq!(JsonPathStack::paths_root().component_base(), None);
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint);
assert_eq!(stack.component_base(), None);
let stack = stack.push("#/components/schemas/User").unwrap();
assert_eq!(stack.component_base(), Some("#/components/schemas/User"));
let stack = stack.append("properties").append("name");
assert_eq!(stack.component_base(), Some("#/components/schemas/User"));
}
#[test]
fn json_path_stack_endpoint() {
let stack = JsonPathStack::paths_root();
assert_eq!(stack.endpoint_base(), None);
let endpoint = EndpointPath::for_operation("/users", "get");
let stack = JsonPathStack::for_endpoint(endpoint);
assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get"));
let stack = stack.append("responses").append("200");
assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get"));
let stack = stack
.append("schema")
.push("#/components/schemas/User")
.unwrap();
assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get"));
let stack = stack
.append("properties")
.push("#/components/schemas/Address")
.unwrap();
assert_eq!(stack.endpoint_base(), Some("#/paths/~1users/get"));
}
}