From eb10788bc8781869ef1c5f16f231aa7cbfef79e9 Mon Sep 17 00:00:00 2001 From: navibyte <88932567+navispatial@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:55:52 +0300 Subject: [PATCH] feat(geodata): access OpenAPI document for OGC API Features service #170 --- README.md | 2 +- dart/geodata/CHANGELOG.md | 4 ++ dart/geodata/README.md | 7 ++- dart/geodata/example/geodata_example.dart | 15 +++++ .../example/ogcapi_features_example.dart | 5 ++ dart/geodata/lib/core.dart | 1 + .../lib/src/core/api/open_api_document.dart | 57 ++++++++++++++++++ .../model/ogc_feature_service.dart | 19 +++++- .../service/client/ogc_feature_client.dart | 58 +++++++++++++++++++ 9 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 dart/geodata/lib/src/core/api/open_api_document.dart diff --git a/README.md b/README.md index a61b1f06..4bc6376e 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Quick start code to access a Web API service conforming to OGC API Features: // 1. Get a client instance for a Web API endpoint. final client = OGCAPIFeatures.http(endpoint: Uri.parse('...')); -// 2. Access (and check) metadata (meta, conformance or collections) as needed. +// 2. Access/check metadata (meta, OpenAPI, conformance, collections) as needed. final conformance = await client.conformance(); if(!conformance.conformsToCore(geoJSON: true)) { return; // not conforming to core and GeoJSON - so return diff --git a/dart/geodata/CHANGELOG.md b/dart/geodata/CHANGELOG.md index c12072fe..bfa856d7 100644 --- a/dart/geodata/CHANGELOG.md +++ b/dart/geodata/CHANGELOG.md @@ -6,6 +6,10 @@ NOTE: Version 0.12.0 currently under development (0.12.0-dev.0). * [Check conformance classes known by OGC API Features #169](https://github.com/navibyte/geospatial/issues/169) * Removed deprecated functions to create GeoJSON and OGC API Features clients. +🧩 Features: +* [Full client-side support for calling OGC API Features service according to Part 1 + 2 #9](https://github.com/navibyte/geospatial/issues/9) +* [Add support for API definition (like Open API 3.0) when accessing OGC API Features clients #170](https://github.com/navibyte/geospatial/issues/170) + 🛠 Maintenance: * Removed extra internal export files and made internal imports more excplicit. diff --git a/dart/geodata/README.md b/dart/geodata/README.md index 6d09b61f..34b70931 100644 --- a/dart/geodata/README.md +++ b/dart/geodata/README.md @@ -78,7 +78,7 @@ for (final feat in features) { // 1. Get a client instance for a Web API endpoint. final client = OGCAPIFeatures.http(endpoint: Uri.parse('...')); -// 2. Access (and check) metadata (meta, conformance or collections) as needed. +// 2. Access/check metadata (meta, OpenAPI, conformance, collections) as needed. final conformance = await client.conformance(); if(!conformance.conformsToCore(geoJSON: true)) { return; // not conforming to core and GeoJSON - so return @@ -264,6 +264,11 @@ Future main(List args) async { final meta = await client.meta(); print('Service: ${meta.title}'); + // access OpenAPI definition for the service and check for terms of service + // (OpenAPI contains also other info of service, queries and responses, etc.) + final info = (await client.openAPI()).meta['info'] as Map; + print('Terms of service: ${info['termsOfService']}'); + // get a feature source (`OGCFeatureSource`) for Dutch windmill point features final source = await client.collection('dutch_windmills'); diff --git a/dart/geodata/example/geodata_example.dart b/dart/geodata/example/geodata_example.dart index df308f7a..0105c686 100644 --- a/dart/geodata/example/geodata_example.dart +++ b/dart/geodata/example/geodata_example.dart @@ -142,6 +142,10 @@ Future main(List args) async { print('OGC API Features service:'); _printMeta(meta); + // read OpenAPI definition + final openAPI = await service.openAPI(); + _printOpenAPI(openAPI); + // read conformance classes final conformance = await service.conformance(); _printConformance(conformance.classes); @@ -258,6 +262,17 @@ void _printMeta(ResourceMeta meta) { } } +void _printOpenAPI(OpenAPIDocument document) { + print('OpenAPI ${document.openapi}'); + final servers = document.meta['servers'] as Iterable; + for (final s in servers) { + final server = s as Map; + final url = server['url']; + final desc = server['description']; + print(' $url : $desc'); + } +} + void _printConformance(Iterable conformance) { print('Conformance classes:'); for (final e in conformance) { diff --git a/dart/geodata/example/ogcapi_features_example.dart b/dart/geodata/example/ogcapi_features_example.dart index 5395f007..cfef31e4 100644 --- a/dart/geodata/example/ogcapi_features_example.dart +++ b/dart/geodata/example/ogcapi_features_example.dart @@ -28,6 +28,11 @@ Future main(List args) async { final meta = await client.meta(); print('Service: ${meta.title}'); + // access OpenAPI definition for the service and check for terms of service + // (OpenAPI contains also other info of service, queries and responses, etc.) + final info = (await client.openAPI()).meta['info'] as Map; + print('Terms of service: ${info['termsOfService']}'); + // conformance classes (text ids) informs the capabilities of the service final conformance = await client.conformance(); print('Conformance classes:'); diff --git a/dart/geodata/lib/core.dart b/dart/geodata/lib/core.dart index 2740ac57..54f0c464 100644 --- a/dart/geodata/lib/core.dart +++ b/dart/geodata/lib/core.dart @@ -9,6 +9,7 @@ /// Usage: import `package:geodata/core.dart` library core; +export 'src/core/api/open_api_document.dart'; export 'src/core/base/collection_meta.dart'; export 'src/core/base/resource_meta.dart'; export 'src/core/data/bounded_items_query.dart'; diff --git a/dart/geodata/lib/src/core/api/open_api_document.dart b/dart/geodata/lib/src/core/api/open_api_document.dart new file mode 100644 index 00000000..ca2a8503 --- /dev/null +++ b/dart/geodata/lib/src/core/api/open_api_document.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved. +// Use of this source code is governed by a “BSD-3-Clause”-style license that is +// specified in the LICENSE file. +// +// Docs: https://github.com/navibyte/geospatial + +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +import '/src/common/meta/meta_aware.dart'; + +/// An OpenAPI document for some service with raw content parsed in [meta]. +/// +/// Note: this class wraps decoded JSON Object from a JSON document containing +/// OpenAPI definition. To utilize such data JSON Object tree in [meta] must be +/// traversed as needed. +/// +/// In future this class might containg also helper methods to access certain +/// objects and fields according to the OpenAPI schema. Currently there is only +/// the [openapi] member, other elements must be accessed from raw data. +/// +/// See also: +/// * https://www.openapis.org/ +/// * https://spec.openapis.org/oas/latest.html +/// * https://swagger.io/specification/ +/// +/// Example: +/// ```dart +/// void _printOpenAPI(OpenAPIDocument document) { +/// print('OpenAPI ${document.openapi}'); +/// final servers = document.meta['servers'] as Iterable; +/// for (final s in servers) { +/// final server = s as Map; +/// final url = server['url']; +/// final desc = server['description']; +/// print(' $url : $desc'); +/// } +/// } +/// ``` +@immutable +class OpenAPIDocument with MetaAware, EquatableMixin { + /// An OpenAPI document for some service with raw content parsed in [meta]. + const OpenAPIDocument({ + Map? meta, + }) : meta = meta ?? const {}; + + /// The OpenAPI document as a data object (ie. data from a JSON Object). + @override + final Map meta; + + /// The version number of the OpenAPI Specification that this OpenAPI document + /// uses. + String get openapi => meta['openapi'] as String; + + @override + List get props => [meta]; +} diff --git a/dart/geodata/lib/src/ogcapi_features/model/ogc_feature_service.dart b/dart/geodata/lib/src/ogcapi_features/model/ogc_feature_service.dart index e56ad770..4e2e6358 100644 --- a/dart/geodata/lib/src/ogcapi_features/model/ogc_feature_service.dart +++ b/dart/geodata/lib/src/ogcapi_features/model/ogc_feature_service.dart @@ -4,6 +4,7 @@ // // Docs: https://github.com/navibyte/geospatial +import '/src/core/api/open_api_document.dart'; import '/src/core/base/collection_meta.dart'; import '/src/core/base/resource_meta.dart'; @@ -15,7 +16,23 @@ abstract class OGCFeatureService { /// Get meta data (or "landing page" information) about this service. Future meta(); - // NOTE: API description: api(); + /// Get an OpenAPI documentation (API definition) for this service. + /// + /// The API definition is retrieved: + /// 1. Get a link from [meta] for the relation "service-desc". + /// 2. Ensure it's type is "application/vnd.oai.openapi+json". + /// 3. Read JSON content from a HTTP service. + /// 4. Decode content received as JSON Object using the standard JSON decoder. + /// 5. Wrap such decoded object in an [OpenAPIDocument] instance. + /// + /// If a service does not provide an OpenAPI definition in JSON or retrieving + /// it fails, then a `ServiceException` is thrown. + /// + /// Most often for an OGC API Features service an API definition is an + /// OpenAPI 3.0 document, but this is not required by the standard. You could + /// also check whether [conformance] suggests a service conforming to + /// `openAPI30` before calling [openAPI]. + Future openAPI(); /// Conformance classes this service is conforming to. Future conformance(); diff --git a/dart/geodata/lib/src/ogcapi_features/service/client/ogc_feature_client.dart b/dart/geodata/lib/src/ogcapi_features/service/client/ogc_feature_client.dart index 75152968..c473484a 100644 --- a/dart/geodata/lib/src/ogcapi_features/service/client/ogc_feature_client.dart +++ b/dart/geodata/lib/src/ogcapi_features/service/client/ogc_feature_client.dart @@ -10,8 +10,11 @@ import 'package:geobase/vector.dart'; import 'package:geobase/vector_data.dart'; import 'package:http/http.dart'; +import '/src/common/links/link.dart'; import '/src/common/links/links.dart'; import '/src/common/paged/paged.dart'; +import '/src/common/service/service_exception.dart'; +import '/src/core/api/open_api_document.dart'; import '/src/core/base/collection_meta.dart'; import '/src/core/base/resource_meta.dart'; import '/src/core/data/bounded_items_query.dart'; @@ -61,6 +64,17 @@ class OGCAPIFeatures { // Private implementation code below. // The implementation may change in future. +// OpenAPI definition resources +const _acceptJSONOpenAPI = { + 'accept': 'application/vnd.oai.openapi+json, application/openapi+json' +}; +const _expectJSONOpenAPI = [ + 'application/vnd.oai.openapi+json', + 'application/openapi+json', + 'application/json', +]; + +// collection items of geospatial features (actual data) const _acceptGeoJSON = {'accept': 'application/geo+json'}; const _expectGeoJSON = ['application/geo+json', 'application/json']; const _nextAndPrevLinkType = 'application/geo+json'; @@ -99,6 +113,50 @@ class _OGCFeatureClientHttp implements OGCFeatureService { ); } + /// Resolve an url that's providing OpenAPI or JSON service description. + Link? _resolveServiceDescLink(Iterable links) { + for (final type in _expectJSONOpenAPI) { + for (final link in links) { + if (link.type?.startsWith(type) ?? false) { + return link; + } + } + } + return null; + } + + @override + Future openAPI() async { + // 1. Get a link from [meta] for the relation "service-desc". + // 2. Ensure it's type is "application/vnd.oai.openapi+json". + final m = await meta(); + final links = m.links.serviceDesc(); + final link = _resolveServiceDescLink(links); + if (link == null) { + throw const ServiceException('No valid service-desc link.'); + } + + // 3. Read JSON content from a HTTP service. + // 4. Decode content received as JSON Object using the standard JSON decoder + // 5. Wrap such decoded object in an [OpenAPIDefinition] instance. + final type = link.type; + if (type != null) { + return adapter.getEntityFromJsonObject( + link.href, + headers: {'accept': type}, + expect: _expectJSONOpenAPI, + toEntity: (data) => OpenAPIDocument(meta: data), + ); + } else { + return adapter.getEntityFromJsonObject( + link.href, + headers: _acceptJSONOpenAPI, + expect: _expectJSONOpenAPI, + toEntity: (data) => OpenAPIDocument(meta: data), + ); + } + } + @override Future conformance() async { // fetch data as JSON Object, and parse conformance classes