1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crate::DocCache;

use openapiv3::ReferenceOr;
use serde::de::DeserializeOwned;

use std::fs::File;
use std::path::{Path, PathBuf};

pub struct ResolvedReference<'a, T>
where
  T: DeserializeOwned,
{
  // The root-relative reference after the fragment and slash (`#/`) (e.g., components/schemas/Foo).
  pub root_rel_ref: &'a str,
  pub target: T,
  pub target_name: &'a str,
}

pub fn resolve_reference<'a, T>(
  referrer_doc_path: &Path,
  reference: &'a str,
  cached_external_docs: &mut DocCache,
) -> (PathBuf, ResolvedReference<'a, T>)
where
  T: DeserializeOwned,
{
  let ref_parts = reference.split('#').collect::<Vec<_>>();
  if ref_parts.len() != 2 || !ref_parts[1].starts_with('/') {
    panic!(
      "invalid reference: {reference} (referrer: {})",
      referrer_doc_path.display()
    );
  }

  let (rel_path, rel_ref) = (ref_parts[0], &ref_parts[1][1..]);
  let doc_path = if ref_parts[0].is_empty() {
    PathBuf::from(referrer_doc_path)
  } else {
    PathBuf::from(referrer_doc_path)
      .parent()
      .unwrap()
      .join(rel_path)
  };
  let doc: &serde_yaml::Mapping =
    cached_external_docs
      .entry(doc_path.clone())
      .or_insert_with(|| {
        println!("cargo:rerun-if-changed={}", doc_path.display());
        let doc_file = File::open(&doc_path)
          .unwrap_or_else(|err| panic!("failed to open {}: {err}", doc_path.to_string_lossy()));
        serde_path_to_error::deserialize(serde_yaml::Deserializer::from_reader(&doc_file))
          .unwrap_or_else(|err| {
            panic!(
              "failed to parse external OpenAPI doc {}: {err}",
              doc_path.display()
            )
          })
      });

  let (reference_target, reference_target_name) =
    rel_ref
      .split('/')
      .fold((doc, ""), |(doc_context, _), ref_component| {
        let target_doc_context = doc_context.get(ref_component).unwrap_or_else(|| {
          panic!(
            "invalid reference `{reference}`: path component `{ref_component}` not found in \
                 {doc_context:#?}"
          )
        });
        if let serde_yaml::Value::Mapping(next_doc_context) = target_doc_context {
          (next_doc_context, ref_component)
        } else {
          panic!(
            "invalid reference `{reference}`: must be a mapping, but found {target_doc_context:#?}"
          );
        }
      });

  let target_ref_or_item: ReferenceOr<T> =
    serde_path_to_error::deserialize(serde_yaml::Value::Mapping(reference_target.to_owned()))
      .unwrap_or_else(|err| {
        panic!(
          "failed to deserialize value referenced by `{reference}` (relative to {}): {err}",
          referrer_doc_path.display()
        )
      });

  match target_ref_or_item {
    ReferenceOr::Reference {
      reference: inner_reference,
    } => {
      // If we ever decide to support this, we probably want to recurse and return the final
      // destination of the reference chain. We'll also need to add cycle detection to avoid the
      // potential for infinite recursion. We should also make sure that after the inline pass
      // completes, there are no reference chains (in particular, schema reference chains) reachable
      // from any of the operations. That way, when we generate the models, we'll know that any
      // schema references point directly to named schemas.
      unimplemented!(
        "reference chains (references to references): `{reference}` -> `{inner_reference}`"
      );
    }
    ReferenceOr::Item(target) => (
      doc_path,
      ResolvedReference {
        root_rel_ref: rel_ref,
        target,
        target_name: reference_target_name,
      },
    ),
  }
}

pub fn resolve_local_reference<'a, T>(
  reference: &'a str,
  openapi_inline: &serde_yaml::Mapping,
) -> ResolvedReference<'a, T>
where
  T: DeserializeOwned,
{
  let ref_parts = reference.split('#').collect::<Vec<_>>();
  if ref_parts.len() != 2 || !ref_parts[1].starts_with('/') {
    panic!("invalid reference: {reference}");
  }

  let (rel_path, rel_ref) = (ref_parts[0], &ref_parts[1][1..]);
  if !rel_path.is_empty() {
    panic!("unexpected non-local reference: {reference}")
  }

  let (reference_target, reference_target_name) =
    rel_ref
      .split('/')
      .fold((openapi_inline, ""), |(doc_context, _), ref_component| {
        let target_doc_context = doc_context.get(ref_component).unwrap_or_else(|| {
          panic!(
            "invalid reference `{reference}`: path component `{ref_component}` not found in \
                 {doc_context:#?}"
          )
        });
        if let serde_yaml::Value::Mapping(next_doc_context) = target_doc_context {
          (next_doc_context, ref_component)
        } else {
          panic!(
            "invalid reference `{reference}`: must be a mapping, but found {target_doc_context:#?}"
          );
        }
      });

  let target_ref_or_item: ReferenceOr<T> =
    serde_path_to_error::deserialize(serde_yaml::Value::Mapping(reference_target.to_owned()))
      .unwrap_or_else(|err| {
        panic!("failed to deserialize local value referenced by `{reference}`: {err}");
      });

  match target_ref_or_item {
    ReferenceOr::Reference {
      reference: inner_reference,
    } => {
      // See note above.
      unimplemented!(
        "reference chains (references to references): `{reference}` -> `{inner_reference}`"
      );
    }
    ReferenceOr::Item(target) => ResolvedReference {
      root_rel_ref: rel_ref,
      target,
      target_name: reference_target_name,
    },
  }
}