import 'dart:io'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import '../diagnostic/diagnostic.dart'; import '../util/utils.dart'; /// A root OpenAPI document with normalized references and dependency metadata. class ResolvedOpenApiSource { /// Creates a resolved source result. const ResolvedOpenApiSource({ required this.source, required this.document, required this.dependencies, }); /// Canonical path or URL of the root source. final String source; /// Resolved root document with external components bundled locally. final Map document; /// Canonical external documents loaded while resolving references. final List dependencies; } /// Loads OpenAPI documents and resolves local and external `$ref` values. class OpenApiReferenceResolver { /// Creates a resolver that reports issues to [diagnostics]. OpenApiReferenceResolver(this.diagnostics); /// Destination for loading and reference-resolution diagnostics. final KoiDiagnosticBag diagnostics; final Map> _documentCache = >{}; final Set _loadedDependencies = {}; final Map _bundledRefs = {}; final Map> _bundledComponents = >{}; final Set _activeInlineRefs = {}; late String _rootSource; late Map _rootDocument; /// Loads [source], resolves its references, and returns dependency metadata. Future resolve(String source) async { _rootSource = _canonicalSource(source); _loadedDependencies ..clear() ..add(_rootSource); _bundledRefs.clear(); _bundledComponents.clear(); _activeInlineRefs.clear(); final rawDocument = await _loadDocument(_rootSource); _rootDocument = _deepCopyMap(rawDocument); final resolvedValue = await _resolveNode( _rootDocument, baseSource: _rootSource, location: r'$', ); final resolvedDocument = resolvedValue is Map ? resolvedValue.cast() : {}; _mergeBundledComponents(resolvedDocument); return ResolvedOpenApiSource( source: _rootSource, document: resolvedDocument, dependencies: _loadedDependencies .where((dependency) => dependency != _rootSource) .toList(growable: false) ..sort(), ); } Future _resolveNode( dynamic value, { required String baseSource, required String location, }) async { if (value is List) { final result = []; for (var index = 0; index < value.length; index += 1) { result.add( await _resolveNode( value[index], baseSource: baseSource, location: '$location[$index]', ), ); } return result; } if (value is! Map) { return value; } final map = value.cast(); final ref = map[r'$ref']; if (ref is String) { final resolvedRef = await _resolveReference( ref, baseSource: baseSource, location: '$location.\$ref', ); final siblings = {}; for (final entry in map.entries.toList(growable: false)) { if (entry.key == r'$ref') { continue; } siblings[entry.key] = await _resolveNode( entry.value, baseSource: baseSource, location: '$location.${entry.key}', ); } if (resolvedRef is Map) { return deepMergeMaps(resolvedRef.cast(), siblings); } if (siblings.isNotEmpty) { diagnostics.error( 'openapi.ref_siblings_on_non_object', 'Reference `$ref` resolves to a non-object value, so sibling fields ' 'cannot be merged.', source: baseSource, location: location, ); } return resolvedRef; } final result = {}; for (final entry in map.entries.toList(growable: false)) { result[entry.key] = await _resolveNode( entry.value, baseSource: baseSource, location: '$location.${entry.key}', ); } return result; } Future _resolveReference( String ref, { required String baseSource, required String location, }) async { final target = _resolveLocation(baseSource, ref); if (target == null) { diagnostics.error( 'openapi.invalid_ref', 'Unable to resolve reference URI `$ref`.', source: baseSource, location: location, ); return {}; } if (target.source == _rootSource) { if (target.fragment.isEmpty) { diagnostics.error( 'openapi.root_document_ref_not_supported', 'Referencing an entire root OpenAPI document is not supported.', source: baseSource, location: location, ); return {}; } return {r'$ref': '#${target.fragment}'}; } _loadedDependencies.add(target.source); final component = _componentPointer(target.fragment); if (component != null) { return _bundleExternalComponent( target, component: component, location: location, ); } final absoluteRef = '${target.source}#${target.fragment}'; if (!_activeInlineRefs.add(absoluteRef)) { diagnostics.error( 'openapi.unrepresentable_external_ref_cycle', 'External cyclic reference `$absoluteRef` does not target a component ' 'and cannot be inlined safely.', source: baseSource, location: location, ); return {}; } try { final document = await _loadDocument(target.source); final targetValue = _resolveJsonPointer(document, target.fragment); if (targetValue == null) { diagnostics.error( 'openapi.unresolved_external_ref', 'Reference target `$ref` does not exist in the external document.', source: target.source, location: '#${target.fragment}', ); return {}; } return _resolveNode( _deepCopy(targetValue), baseSource: target.source, location: '${target.source}#${target.fragment}', ); } finally { _activeInlineRefs.remove(absoluteRef); } } Future> _bundleExternalComponent( _ReferenceTarget target, { required _ComponentPointer component, required String location, }) async { final absoluteRef = '${target.source}#${target.fragment}'; final existingRef = _bundledRefs[absoluteRef]; if (existingRef != null) { return {r'$ref': existingRef}; } final localName = _allocateComponentName( component.kind, component.name, target.source, ); final localRef = '#/components/${_escapePointerSegment(component.kind)}/' '${_escapePointerSegment(localName)}'; _bundledRefs[absoluteRef] = localRef; final group = _bundledComponents.putIfAbsent( component.kind, () => {}, ); group[localName] = {}; final document = await _loadDocument(target.source); final targetValue = _resolveJsonPointer(document, target.fragment); if (targetValue == null) { diagnostics.error( 'openapi.unresolved_external_ref', 'Component `${component.kind}/${component.name}` does not exist in ' 'the external document.', source: target.source, location: '#${target.fragment}', ); return {r'$ref': localRef}; } final resolvedValue = await _resolveNode( _deepCopy(targetValue), baseSource: target.source, location: '${target.source}#${target.fragment}', ); if (resolvedValue is! Map) { diagnostics.error( 'openapi.invalid_external_component', 'External component `${component.kind}/${component.name}` must be an ' 'object.', source: target.source, location: location, ); } else { group[localName] = resolvedValue.cast(); } return {r'$ref': localRef}; } String _allocateComponentName( String kind, String preferredName, String source, ) { final rootComponents = _rootDocument['components']; final rootGroup = rootComponents is Map ? rootComponents[kind] : null; final existingNames = { if (rootGroup is Map) ...rootGroup.keys.map((key) => key.toString()), ...?_bundledComponents[kind]?.keys, }; if (!existingNames.contains(preferredName)) { return preferredName; } final sourceStem = _sourceStem(source); final baseName = '${sourceStem}_$preferredName'; var candidate = baseName; var suffix = 1; while (existingNames.contains(candidate)) { suffix += 1; candidate = '$baseName$suffix'; } diagnostics.warning( 'openapi.external_component_renamed', 'External component `$preferredName` conflicts with the root document ' 'and was renamed to `$candidate`.', source: source, location: '#/components/$kind/$preferredName', ); return candidate; } void _mergeBundledComponents(Map document) { if (_bundledComponents.isEmpty) { return; } final components = document.putIfAbsent( 'components', () => {}, ); if (components is! Map) { diagnostics.error( 'openapi.invalid_components', 'Unable to add the external reference because the root document ' 'components value is not an object.', source: _rootSource, location: r'$.components', ); return; } final componentMap = components.cast(); for (final entry in _bundledComponents.entries) { final rawGroup = componentMap.putIfAbsent( entry.key, () => {}, ); if (rawGroup is! Map) { diagnostics.error( 'openapi.invalid_component_group', 'components.${entry.key} must be an object.', source: _rootSource, location: '\$.components.${entry.key}', ); continue; } rawGroup.cast().addAll(entry.value); } } Future> _loadDocument(String source) async { final cached = _documentCache[source]; if (cached != null) { return cached; } try { final String content; if (isHttpUrl(source)) { final response = await http.get(Uri.parse(source)); if (response.statusCode < 200 || response.statusCode >= 300) { diagnostics.error( 'openapi.source_http_error', 'Failed to fetch the OpenAPI document: HTTP ' '${response.statusCode}.', source: source, ); return {}; } content = response.body; } else { final filePath = source.startsWith('file://') ? Uri.parse(source).toFilePath() : source; content = await File(filePath).readAsString(); } final document = decodeStructuredDocument(content); _documentCache[source] = document; return document; } on Object catch (error) { diagnostics.error( 'openapi.source_load_failed', 'Failed to read or parse the OpenAPI document: $error', source: source, ); return {}; } } _ReferenceTarget? _resolveLocation(String baseSource, String ref) { final hashIndex = ref.indexOf('#'); final rawPath = hashIndex < 0 ? ref : ref.substring(0, hashIndex); final fragment = hashIndex < 0 ? '' : ref.substring(hashIndex + 1); try { if (rawPath.isEmpty) { return _ReferenceTarget( source: _canonicalSource(baseSource), fragment: fragment, ); } if (isHttpUrl(rawPath) || rawPath.startsWith('file://')) { return _ReferenceTarget( source: _canonicalSource(rawPath), fragment: fragment, ); } if (isHttpUrl(baseSource)) { final resolved = Uri.parse(baseSource).resolve(rawPath); return _ReferenceTarget( source: _canonicalSource(resolved.toString()), fragment: fragment, ); } final basePath = baseSource.startsWith('file://') ? Uri.parse(baseSource).toFilePath() : baseSource; final targetPath = p.isAbsolute(rawPath) ? rawPath : p.join(p.dirname(basePath), Uri.decodeComponent(rawPath)); return _ReferenceTarget( source: p.normalize(p.absolute(targetPath)), fragment: fragment, ); } on FormatException { return null; } } String _canonicalSource(String source) { if (isHttpUrl(source)) { final fragmentIndex = source.indexOf('#'); final withoutFragment = fragmentIndex < 0 ? source : source.substring(0, fragmentIndex); return Uri.parse(withoutFragment).toString(); } if (source.startsWith('file://')) { return Uri.file(p.normalize(Uri.parse(source).toFilePath())).toString(); } return p.normalize(p.absolute(source)); } _ComponentPointer? _componentPointer(String fragment) { if (!fragment.startsWith('/')) { return null; } final segments = fragment .substring(1) .split('/') .map(_decodePointerSegment) .toList(growable: false); if (segments.length != 3 || segments.first != 'components') { return null; } return _ComponentPointer(kind: segments[1], name: segments[2]); } dynamic _resolveJsonPointer(Map document, String fragment) { if (fragment.isEmpty) { return document; } if (!fragment.startsWith('/')) { return null; } dynamic current = document; for (final rawSegment in fragment.substring(1).split('/')) { final segment = _decodePointerSegment(rawSegment); if (current is Map) { if (!current.containsKey(segment)) { return null; } current = current[segment]; continue; } if (current is List) { final index = int.tryParse(segment); if (index == null || index < 0 || index >= current.length) { return null; } current = current[index]; continue; } return null; } return current; } String _decodePointerSegment(String value) { return Uri.decodeComponent( value.replaceAll('~1', '/').replaceAll('~0', '~'), ); } String _escapePointerSegment(String value) { return value.replaceAll('~', '~0').replaceAll('/', '~1'); } String _sourceStem(String source) { final path = isHttpUrl(source) ? Uri.parse(source).path : source.startsWith('file://') ? Uri.parse(source).toFilePath() : source; final stem = p.basenameWithoutExtension(path); return stem.isEmpty ? 'external' : toSnakeCase(stem); } Map _deepCopyMap(Map value) { return _deepCopy(value) as Map; } dynamic _deepCopy(dynamic value) { if (value is Map) { return value.map( (key, dynamic item) => MapEntry(key.toString(), _deepCopy(item)), ); } if (value is List) { return value.map(_deepCopy).toList(growable: true); } return value; } } class _ReferenceTarget { const _ReferenceTarget({required this.source, required this.fragment}); final String source; final String fragment; } class _ComponentPointer { const _ComponentPointer({required this.kind, required this.name}); final String kind; final String name; }