1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::path::Path;
6use std::path::PathBuf;
7use std::rc::Rc;
8
9use indexmap::IndexMap;
10use openapiv3::OpenAPI;
11use openapiv3::Parameter;
12use openapiv3::ReferenceOr;
13use openapiv3::RequestBody;
14use openapiv3::Response;
15use openapiv3::Schema;
16
17use crate::error::Error;
18use crate::error::Result;
19
20const MAX_REF_DEPTH: usize = 32;
22
23const SUPPORTED_SPEC_VERSIONS: [&str; 1] = ["3.0"];
29
30const UNSUPPORTED_TOP_LEVEL_KEYS: [&str; 1] = ["webhooks"];
39
40static EMPTY_SCHEMAS: std::sync::OnceLock<IndexMap<String, ReferenceOr<Schema>>> = std::sync::OnceLock::new();
42
43static EMPTY_SECURITY_SCHEMES: std::sync::OnceLock<IndexMap<String, ReferenceOr<openapiv3::SecurityScheme>>> =
45 std::sync::OnceLock::new();
46
47#[derive(Debug, Clone)]
49pub struct Resolved<T> {
50 pub value: T,
52 pub origin: Option<String>,
56}
57
58#[derive(Debug)]
60pub struct Spec {
61 inner: OpenAPI,
62 source: PathBuf,
63 docs: RefCell<HashMap<PathBuf, Rc<OpenAPI>>>,
64}
65
66impl Spec {
67 pub fn load(path: &Path) -> Result<Self> {
69 let text = std::fs::read_to_string(path).map_err(|source| {
70 return Error::ReadSpec {
71 path: path.display().to_string(),
72 source,
73 };
74 })?;
75 let document = path.display().to_string();
76 let value: serde_yaml::Value = serde_yaml::from_str(&text).map_err(|source| {
81 return Error::ParseSpec {
82 path: document.clone(),
83 source,
84 };
85 })?;
86 check_spec_version(&document, &value)?;
90 check_top_level_keys(&value)?;
91 let inner: OpenAPI = serde_yaml::from_value(value).map_err(|source| {
92 return Error::ParseSpec {
93 path: document.clone(),
94 source,
95 };
96 })?;
97 return Ok(Spec {
98 inner,
99 source: path.to_path_buf(),
100 docs: RefCell::new(HashMap::new()),
101 });
102 }
103
104 pub fn from_parts(inner: OpenAPI, source: PathBuf) -> Self {
106 return Spec {
107 inner,
108 source,
109 docs: RefCell::new(HashMap::new()),
110 };
111 }
112
113 fn document_for(&self, file: &str) -> Result<Rc<OpenAPI>> {
116 let base = self.source.parent().unwrap_or_else(|| {
117 return Path::new(".");
118 });
119 let path = base.join(file);
120 if let Some(doc) = self.docs.borrow().get(&path) {
121 return Ok(Rc::clone(doc));
122 }
123 let text = std::fs::read_to_string(&path).map_err(|source| {
124 return Error::ReadRefFile {
125 file: file.to_owned(),
126 source,
127 };
128 })?;
129 let value: serde_yaml::Value = serde_yaml::from_str(&text).map_err(|source| {
130 return Error::ParseRefFile {
131 file: file.to_owned(),
132 source,
133 };
134 })?;
135 check_spec_version(file, &value)?;
140 check_top_level_keys(&value)?;
141 let parsed: OpenAPI = serde_yaml::from_value(value).map_err(|source| {
142 return Error::ParseRefFile {
143 file: file.to_owned(),
144 source,
145 };
146 })?;
147 let doc = Rc::new(parsed);
148 self.docs.borrow_mut().insert(path, Rc::clone(&doc));
149 return Ok(doc);
150 }
151
152 pub fn source(&self) -> &Path {
154 return &self.source;
155 }
156
157 pub fn apply_filters(&mut self, opts: &crate::config::OutputOptions) {
160 crate::filter::apply(&mut self.inner, opts);
161 }
162
163 pub fn schemas(&self) -> &IndexMap<String, ReferenceOr<Schema>> {
165 let empty = EMPTY_SCHEMAS.get_or_init(IndexMap::new);
166 let schemas = self
167 .inner
168 .components
169 .as_ref()
170 .map(|c| {
171 return &c.schemas;
172 })
173 .unwrap_or(empty);
174 return schemas;
175 }
176
177 pub fn paths(&self) -> &openapiv3::Paths {
179 return &self.inner.paths;
180 }
181
182 pub fn servers(&self) -> &[openapiv3::Server] {
184 return &self.inner.servers;
185 }
186
187 pub fn global_security(&self) -> Option<&[openapiv3::SecurityRequirement]> {
190 return self.inner.security.as_deref();
191 }
192
193 pub fn security_schemes(&self) -> &IndexMap<String, ReferenceOr<openapiv3::SecurityScheme>> {
196 let empty = EMPTY_SECURITY_SCHEMES.get_or_init(IndexMap::new);
197 let schemes = self
198 .inner
199 .components
200 .as_ref()
201 .map(|c| {
202 return &c.security_schemes;
203 })
204 .unwrap_or(empty);
205 return schemes;
206 }
207
208 pub fn resolve_response(&self, reference: &str) -> Result<Resolved<Response>> {
212 let mut current = reference.to_owned();
213 let mut origin: Option<String> = None;
214 for _ in 0..MAX_REF_DEPTH {
215 if let Some(file) = ref_file_part(¤t) {
216 origin = Some(file.to_owned());
217 }
218 let name = ref_component_name(¤t, "responses").ok_or_else(|| {
219 return Error::UnsupportedRef {
220 reference: current.clone(),
221 reason: "only `#/components/responses/<name>` references are supported".to_owned(),
222 };
223 })?;
224 let entry = self.component_response(origin.as_deref(), ¤t, name)?;
225 match entry {
226 ReferenceOr::Item(response) => {
227 return Ok(Resolved {
228 value: response,
229 origin,
230 });
231 }
232 ReferenceOr::Reference { reference } => {
233 current = reference;
234 }
235 }
236 }
237 return Err(Error::UnresolvedRef(reference.to_owned()));
238 }
239
240 pub fn resolve_parameter(&self, reference: &str) -> Result<Resolved<Parameter>> {
244 let mut current = reference.to_owned();
245 let mut origin: Option<String> = None;
246 for _ in 0..MAX_REF_DEPTH {
247 if let Some(file) = ref_file_part(¤t) {
248 origin = Some(file.to_owned());
249 }
250 let name = ref_component_name(¤t, "parameters").ok_or_else(|| {
251 return Error::UnsupportedRef {
252 reference: current.clone(),
253 reason: "only `#/components/parameters/<name>` references are supported".to_owned(),
254 };
255 })?;
256 let entry = self.component_parameter(origin.as_deref(), ¤t, name)?;
257 match entry {
258 ReferenceOr::Item(parameter) => {
259 return Ok(Resolved {
260 value: parameter,
261 origin,
262 });
263 }
264 ReferenceOr::Reference { reference } => {
265 current = reference;
266 }
267 }
268 }
269 return Err(Error::UnresolvedRef(reference.to_owned()));
270 }
271
272 pub fn resolve_request_body(&self, reference: &str) -> Result<Resolved<RequestBody>> {
276 let mut current = reference.to_owned();
277 let mut origin: Option<String> = None;
278 for _ in 0..MAX_REF_DEPTH {
279 if let Some(file) = ref_file_part(¤t) {
280 origin = Some(file.to_owned());
281 }
282 let name = ref_component_name(¤t, "requestBodies").ok_or_else(|| {
283 return Error::UnsupportedRef {
284 reference: current.clone(),
285 reason: "only `#/components/requestBodies/<name>` references are supported".to_owned(),
286 };
287 })?;
288 let entry = self.component_request_body(origin.as_deref(), ¤t, name)?;
289 match entry {
290 ReferenceOr::Item(body) => {
291 return Ok(Resolved { value: body, origin });
292 }
293 ReferenceOr::Reference { reference } => {
294 current = reference;
295 }
296 }
297 }
298 return Err(Error::UnresolvedRef(reference.to_owned()));
299 }
300
301 pub fn resolve(&self, reference: &str) -> Result<&Schema> {
304 let mut current = reference.to_owned();
305 for _ in 0..MAX_REF_DEPTH {
306 let name = ref_target_name(¤t).ok_or_else(|| {
307 return Error::UnsupportedRef {
308 reference: current.clone(),
309 reason: "only `#/components/schemas/<name>` references are supported".to_owned(),
310 };
311 })?;
312 let entry = self
313 .schemas()
314 .get(name)
315 .ok_or_else(|| return Error::UnresolvedRef(current.clone()))?;
316 match entry {
317 ReferenceOr::Item(schema) => {
318 return Ok(schema);
319 }
320 ReferenceOr::Reference { reference } => {
321 current = reference.clone();
322 }
323 }
324 }
325 return Err(Error::UnresolvedRef(reference.to_owned()));
326 }
327
328 pub fn resolve_schema(&self, origin: Option<&str>, reference: &str) -> Result<Schema> {
337 let mut current = reference.to_owned();
338 for _ in 0..MAX_REF_DEPTH {
339 if ref_file_part(¤t).is_some() {
340 return Err(Error::UnsupportedRef {
341 reference: current.clone(),
342 reason: "cross-file schema `$ref`s are not supported here".to_owned(),
343 });
344 }
345 let name = ref_component_name(¤t, "schemas").ok_or_else(|| {
346 return Error::UnsupportedRef {
347 reference: current.clone(),
348 reason: "only `#/components/schemas/<name>` references are supported".to_owned(),
349 };
350 })?;
351 let entry = self.component_schema(origin, ¤t, name)?;
352 match entry {
353 ReferenceOr::Item(schema) => {
354 return Ok(schema);
355 }
356 ReferenceOr::Reference { reference } => {
357 current = reference;
358 }
359 }
360 }
361 return Err(Error::UnresolvedRef(reference.to_owned()));
362 }
363
364 fn component_lookup<T>(
372 &self,
373 origin: Option<&str>,
374 reference: &str,
375 select: impl Fn(&OpenAPI) -> Option<ReferenceOr<T>>,
376 ) -> Result<ReferenceOr<T>> {
377 let entry = match origin {
378 None => select(&self.inner),
379 Some(file) => {
380 let doc = self.document_for(file)?;
381 select(doc.as_ref())
382 }
383 };
384 return entry.ok_or_else(|| return Error::UnresolvedRef(reference.to_owned()));
385 }
386
387 fn component_response(&self, origin: Option<&str>, reference: &str, name: &str) -> Result<ReferenceOr<Response>> {
389 return self.component_lookup(origin, reference, |doc| {
390 return doc.components.as_ref().and_then(|components| {
391 return components.responses.get(name).cloned();
392 });
393 });
394 }
395
396 fn component_parameter(&self, origin: Option<&str>, reference: &str, name: &str) -> Result<ReferenceOr<Parameter>> {
398 return self.component_lookup(origin, reference, |doc| {
399 return doc.components.as_ref().and_then(|components| {
400 return components.parameters.get(name).cloned();
401 });
402 });
403 }
404
405 fn component_request_body(
407 &self,
408 origin: Option<&str>,
409 reference: &str,
410 name: &str,
411 ) -> Result<ReferenceOr<RequestBody>> {
412 return self.component_lookup(origin, reference, |doc| {
413 return doc.components.as_ref().and_then(|components| {
414 return components.request_bodies.get(name).cloned();
415 });
416 });
417 }
418
419 fn component_schema(&self, origin: Option<&str>, reference: &str, name: &str) -> Result<ReferenceOr<Schema>> {
421 return self.component_lookup(origin, reference, |doc| {
422 return doc.components.as_ref().and_then(|components| {
423 return components.schemas.get(name).cloned();
424 });
425 });
426 }
427
428 pub fn external_schema_name(&self, file: &str, name: &str, reference: &str) -> Result<String> {
445 let entry = self.component_schema(Some(file), reference, name)?;
446 let chosen = external_name_of(&entry, name)?;
447 let doc = self.document_for(file)?;
448 let schemas = doc.components.as_ref().map(|components| return &components.schemas);
449 for (other, other_entry) in schemas.into_iter().flatten() {
450 if other == name {
451 continue;
452 }
453 let taken = external_name_of(other_entry, other)?;
454 let ident = crate::naming::to_ident(&chosen, crate::naming::Case::Pascal);
455 if crate::naming::to_ident(&taken, crate::naming::Case::Pascal).logical() == ident.logical() {
456 return Err(Error::UnsupportedRef {
457 reference: reference.to_owned(),
458 reason: format!(
459 "`{file}` gives `{name}` and `{other}` the one Rust name `{}`",
460 ident.logical()
461 ),
462 });
463 }
464 }
465 return Ok(chosen);
466 }
467}
468
469fn external_name_of(entry: &ReferenceOr<Schema>, name: &str) -> Result<String> {
472 let renamed = match entry {
473 ReferenceOr::Item(schema) => {
474 crate::lower::extension::str_value(&schema.schema_data.extensions, crate::naming::X_RUST_NAME, name)?
475 }
476 ReferenceOr::Reference { .. } => None,
477 };
478 return Ok(renamed.unwrap_or(name).to_owned());
479}
480
481fn check_spec_version(document: &str, value: &serde_yaml::Value) -> Result<()> {
497 let Some(version) = value.get("openapi").and_then(serde_yaml::Value::as_str) else {
498 return Ok(());
499 };
500 let version = version.trim();
501 let supported = SUPPORTED_SPEC_VERSIONS.iter().any(|minor| {
504 return version == *minor || version.starts_with(&format!("{minor}."));
505 });
506 if supported {
507 return Ok(());
508 }
509 let reads = SUPPORTED_SPEC_VERSIONS
510 .iter()
511 .map(|minor| {
512 return format!("{minor}.x");
513 })
514 .collect::<Vec<_>>()
515 .join(", ");
516 return Err(Error::UnsupportedSpecVersion {
517 document: document.to_owned(),
518 version: version.to_owned(),
519 hint: format!("The generator reads OpenAPI {reads} only."),
520 });
521}
522
523fn check_top_level_keys(value: &serde_yaml::Value) -> Result<()> {
531 let Some(mapping) = value.as_mapping() else {
532 return Ok(());
533 };
534 for key in UNSUPPORTED_TOP_LEVEL_KEYS {
535 if mapping.contains_key(serde_yaml::Value::String(key.to_owned())) {
536 return Err(Error::UnsupportedSpecKey {
537 key: key.to_owned(),
538 reason: "declares operations the generator cannot emit".to_owned(),
539 hint: format!(
540 "Remove `{key}:`, or move its operations under `paths:`. Silently ignoring it emits no handler for any operation it holds."
541 ),
542 });
543 }
544 }
545 return Ok(());
546}
547
548pub fn ref_target_name(reference: &str) -> Option<&str> {
557 if ref_file_part(reference).is_some() {
558 return None;
559 }
560 return ref_component_name(reference, "schemas");
561}
562
563pub fn schema_ref_reason(reference: &str, site: &str) -> String {
571 if ref_file_part(reference).is_some() {
572 return format!("a cross-file ref does not resolve at {site}");
573 }
574 return format!("{site} must reference `#/components/schemas/<name>`");
575}
576
577pub fn ref_component_name<'a>(reference: &'a str, kind: &str) -> Option<&'a str> {
581 let fragment = reference.split('#').nth(1).unwrap_or(reference);
582 let prefix = match kind {
583 "schemas" => "/components/schemas/",
584 "responses" => "/components/responses/",
585 "parameters" => "/components/parameters/",
586 "requestBodies" => "/components/requestBodies/",
587 _ => return None,
588 };
589 let name = fragment.strip_prefix(prefix)?;
590 if name.is_empty() || name.contains('/') {
591 return None;
592 }
593 return Some(name);
594}
595
596pub fn ref_file_part(reference: &str) -> Option<&str> {
599 return match reference.split_once('#') {
600 Some((file, _fragment)) if !file.is_empty() => Some(file),
601 _ => None,
602 };
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 struct TestDir {
610 path: PathBuf,
611 }
612
613 impl TestDir {
614 fn new(test_name: &str) -> Self {
615 let unique = format!(
616 "oapi-codegen-loader-{test_name}-{}-{}",
617 std::process::id(),
618 std::time::SystemTime::now()
619 .duration_since(std::time::UNIX_EPOCH)
620 .expect("system clock should be after Unix epoch")
621 .as_nanos(),
622 );
623 let path = std::env::temp_dir().join(unique);
624 std::fs::create_dir_all(&path).expect("create test directory");
625 return Self { path };
626 }
627
628 fn write(&self, file: &str, contents: &str) -> PathBuf {
629 let path = self.path.join(file);
630 if let Some(parent) = path.parent() {
631 std::fs::create_dir_all(parent).expect("create parent directory");
632 }
633 std::fs::write(&path, contents).expect("write test file");
634 return path;
635 }
636 }
637
638 impl Drop for TestDir {
639 fn drop(&mut self) {
640 let _ = std::fs::remove_dir_all(&self.path);
641 }
642 }
643
644 fn parse_openapi(yaml: &str) -> OpenAPI {
645 return serde_yaml::from_str(yaml).expect("parse OpenAPI document");
646 }
647
648 fn assert_query_parameter_name(parameter: &Parameter, expected: &str) {
649 match parameter {
650 Parameter::Query { parameter_data, .. } => {
651 assert_eq!(parameter_data.name, expected);
652 }
653 _ => panic!("expected a query parameter"),
654 }
655 }
656
657 fn minimal_doc() -> &'static str {
658 return "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\n";
659 }
660
661 fn shared_parameter_doc(name: &str) -> String {
662 return format!(
663 "openapi: 3.0.3\ninfo:\n title: shared\n version: '1'\npaths: {{}}\ncomponents:\n parameters:\n PageSize:\n name: {name}\n in: query\n schema:\n type: integer\n",
664 );
665 }
666
667 #[test]
668 fn ref_target_name_is_same_document_schemas_only() {
669 assert_eq!(ref_target_name("#/components/schemas/Foo"), Some("Foo"));
670 assert_eq!(
673 ref_target_name("schemas/common.yaml#/components/schemas/ErrorResponse"),
674 None
675 );
676 assert_eq!(ref_target_name("#/components/responses/Bar"), None);
677 }
678
679 #[test]
682 fn a_schema_ref_reason_names_the_fault() {
683 assert_eq!(
684 schema_ref_reason("common.yaml#/components/schemas/X", "a property"),
685 "a cross-file ref does not resolve at a property"
686 );
687 assert_eq!(
688 schema_ref_reason("#/components/responses/Bar", "a property"),
689 "a property must reference `#/components/schemas/<name>`"
690 );
691 }
692
693 #[test]
694 fn extracts_component_responses_and_file_parts() {
695 assert_eq!(
696 ref_component_name("#/components/responses/Bar", "responses"),
697 Some("Bar")
698 );
699 assert_eq!(ref_component_name("#/components/schemas/Foo", "responses"), None);
700 assert_eq!(ref_file_part("#/components/schemas/Foo"), None);
701 assert_eq!(
702 ref_file_part("schemas/common.yaml#/components/schemas/ErrorResponse"),
703 Some("schemas/common.yaml"),
704 );
705 }
706
707 #[test]
708 fn ref_component_name_recognizes_parameters_and_request_bodies() {
709 assert_eq!(
710 ref_component_name("#/components/parameters/PageSize", "parameters"),
711 Some("PageSize")
712 );
713 assert_eq!(
714 ref_component_name("#/components/requestBodies/CreateWidget", "requestBodies"),
715 Some("CreateWidget")
716 );
717 assert_eq!(ref_component_name("#/components/schemas/Foo", "parameters"), None);
718 }
719
720 #[test]
721 fn resolves_same_document_component_parameter() {
722 let yaml = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n parameters:\n PageSize:\n name: pageSize\n in: query\n schema:\n type: integer\n";
723 let doc = parse_openapi(yaml);
724 let spec = Spec::from_parts(doc, std::path::PathBuf::from("inline.yaml"));
725 let param = spec
726 .resolve_parameter("#/components/parameters/PageSize")
727 .expect("resolve");
728 assert!(param.origin.is_none());
729 assert_query_parameter_name(¶m.value, "pageSize");
730 }
731
732 #[test]
733 fn errors_on_cross_file_ref_to_missing_file() {
734 let source = std::env::temp_dir().join(format!(
735 "oapi-codegen-loader-missing-{}-{}.yaml",
736 std::process::id(),
737 std::time::SystemTime::now()
738 .duration_since(std::time::UNIX_EPOCH)
739 .expect("system clock should be after Unix epoch")
740 .as_nanos(),
741 ));
742 let spec = Spec::from_parts(parse_openapi(minimal_doc()), source);
743 let result = spec.resolve_parameter("common.yaml#/components/parameters/PageSize");
744 assert!(matches!(result, Err(Error::ReadRefFile { .. })));
745 }
746
747 #[test]
748 fn unresolved_component_error_preserves_the_full_reference() {
749 let yaml = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n parameters: {}\n";
752 let spec = Spec::from_parts(parse_openapi(yaml), std::path::PathBuf::from("inline.yaml"));
753 let result = spec.resolve_parameter("#/components/parameters/Missing");
754 match result {
755 Err(Error::UnresolvedRef(reference)) => {
756 assert_eq!(reference, "#/components/parameters/Missing");
757 }
758 other => panic!("expected UnresolvedRef, got {other:?}"),
759 }
760
761 let dir = TestDir::new("unresolved-cross-file");
763 let main = dir.write("main.yaml", minimal_doc());
764 dir.write("shared.yaml", minimal_doc());
765 let spec = Spec::load(&main).expect("load main spec");
766 let result = spec.resolve_parameter("shared.yaml#/components/parameters/Missing");
767 match result {
768 Err(Error::UnresolvedRef(reference)) => {
769 assert_eq!(reference, "shared.yaml#/components/parameters/Missing");
770 }
771 other => panic!("expected UnresolvedRef, got {other:?}"),
772 }
773 }
774
775 #[test]
776 fn document_for_reads_sibling_and_caches() {
777 let dir = TestDir::new("document-for-caches");
778 let main = dir.write("main.yaml", minimal_doc());
779 dir.write("shared.yaml", &shared_parameter_doc("pageSize"));
780
781 let spec = Spec::load(&main).expect("load main spec");
782 let param = spec
783 .resolve_parameter("shared.yaml#/components/parameters/PageSize")
784 .expect("resolve cross-file parameter");
785 assert_eq!(param.origin.as_deref(), Some("shared.yaml"));
786 assert_query_parameter_name(¶m.value, "pageSize");
787 assert_eq!(spec.docs.borrow().len(), 1);
788
789 let second = spec
790 .resolve_parameter("shared.yaml#/components/parameters/PageSize")
791 .expect("resolve cached cross-file parameter");
792 assert_eq!(second.origin.as_deref(), Some("shared.yaml"));
793 assert_query_parameter_name(&second.value, "pageSize");
794 assert_eq!(spec.docs.borrow().len(), 1);
795 }
796
797 #[test]
798 fn cross_file_chain_across_two_files_resolves() {
799 let dir = TestDir::new("cross-file-chain");
800 let main = dir.write("main.yaml", minimal_doc());
801 dir.write(
802 "a.yaml",
803 "openapi: 3.0.3\ninfo:\n title: a\n version: '1'\npaths: {}\ncomponents:\n parameters:\n X:\n $ref: \"b.yaml#/components/parameters/Y\"\n",
804 );
805 dir.write(
806 "b.yaml",
807 "openapi: 3.0.3\ninfo:\n title: b\n version: '1'\npaths: {}\ncomponents:\n parameters:\n Y:\n name: cursor\n in: query\n schema:\n type: string\n",
808 );
809
810 let spec = Spec::load(&main).expect("load main spec");
811 let param = spec
812 .resolve_parameter("a.yaml#/components/parameters/X")
813 .expect("resolve cross-file chain");
814 assert_eq!(param.origin.as_deref(), Some("b.yaml"));
815 assert_query_parameter_name(¶m.value, "cursor");
816 }
817
818 #[test]
819 fn cross_file_cycle_terminates() {
820 let dir = TestDir::new("cross-file-cycle");
821 let main = dir.write("main.yaml", minimal_doc());
822 dir.write(
823 "a.yaml",
824 "openapi: 3.0.3\ninfo:\n title: a\n version: '1'\npaths: {}\ncomponents:\n parameters:\n X:\n $ref: \"b.yaml#/components/parameters/Y\"\n",
825 );
826 dir.write(
827 "b.yaml",
828 "openapi: 3.0.3\ninfo:\n title: b\n version: '1'\npaths: {}\ncomponents:\n parameters:\n Y:\n $ref: \"a.yaml#/components/parameters/X\"\n",
829 );
830
831 let spec = Spec::load(&main).expect("load main spec");
832 let result = spec.resolve_parameter("a.yaml#/components/parameters/X");
833 assert!(matches!(result, Err(Error::UnresolvedRef(_))));
834 }
835
836 #[test]
837 fn resolve_schema_against_origin() {
838 let dir = TestDir::new("resolve-schema-origin");
839 let main = dir.write("main.yaml", minimal_doc());
840 dir.write(
841 "shared.yaml",
842 "openapi: 3.0.3\ninfo:\n title: shared\n version: '1'\npaths: {}\ncomponents:\n schemas:\n PageInfo:\n type: integer\n format: int32\n",
843 );
844
845 let spec = Spec::load(&main).expect("load main spec");
846 let schema = spec
847 .resolve_schema(Some("shared.yaml"), "#/components/schemas/PageInfo")
848 .expect("resolve schema from origin");
849 assert!(matches!(
850 schema.schema_kind,
851 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_))
852 ));
853 }
854}