Skip to content

Commit

Permalink
feat(geodata): access OpenAPI document for OGC API Features service #170
Browse files Browse the repository at this point in the history
  • Loading branch information
navispatial committed Jul 20, 2023
1 parent fce5d23 commit eb10788
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions dart/geodata/CHANGELOG.md
Expand Up @@ -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.

Expand Down
7 changes: 6 additions & 1 deletion dart/geodata/README.md
Expand Up @@ -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
Expand Down Expand Up @@ -264,6 +264,11 @@ Future<void> main(List<String> 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<String, dynamic>;
print('Terms of service: ${info['termsOfService']}');
// get a feature source (`OGCFeatureSource`) for Dutch windmill point features
final source = await client.collection('dutch_windmills');
Expand Down
15 changes: 15 additions & 0 deletions dart/geodata/example/geodata_example.dart
Expand Up @@ -142,6 +142,10 @@ Future<void> main(List<String> 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);
Expand Down Expand Up @@ -258,6 +262,17 @@ void _printMeta(ResourceMeta meta) {
}
}

void _printOpenAPI(OpenAPIDocument document) {
print('OpenAPI ${document.openapi}');
final servers = document.meta['servers'] as Iterable<dynamic>;
for (final s in servers) {
final server = s as Map<String, dynamic>;
final url = server['url'];
final desc = server['description'];
print(' $url : $desc');
}
}

void _printConformance(Iterable<String> conformance) {
print('Conformance classes:');
for (final e in conformance) {
Expand Down
5 changes: 5 additions & 0 deletions dart/geodata/example/ogcapi_features_example.dart
Expand Up @@ -28,6 +28,11 @@ Future<void> main(List<String> 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<String, dynamic>;
print('Terms of service: ${info['termsOfService']}');

// conformance classes (text ids) informs the capabilities of the service
final conformance = await client.conformance();
print('Conformance classes:');
Expand Down
1 change: 1 addition & 0 deletions dart/geodata/lib/core.dart
Expand Up @@ -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';
Expand Down
57 changes: 57 additions & 0 deletions 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<dynamic>;
/// for (final s in servers) {
/// final server = s as Map<String, dynamic>;
/// 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<String, dynamic>? meta,
}) : meta = meta ?? const {};

/// The OpenAPI document as a data object (ie. data from a JSON Object).
@override
final Map<String, dynamic> meta;

/// The version number of the OpenAPI Specification that this OpenAPI document
/// uses.
String get openapi => meta['openapi'] as String;

@override
List<Object?> get props => [meta];
}
Expand Up @@ -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';

Expand All @@ -15,7 +16,23 @@ abstract class OGCFeatureService {
/// Get meta data (or "landing page" information) about this service.
Future<ResourceMeta> 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<OpenAPIDocument> openAPI();

/// Conformance classes this service is conforming to.
Future<OGCFeatureConformance> conformance();
Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -99,6 +113,50 @@ class _OGCFeatureClientHttp implements OGCFeatureService {
);
}

/// Resolve an url that's providing OpenAPI or JSON service description.
Link? _resolveServiceDescLink(Iterable<Link> links) {
for (final type in _expectJSONOpenAPI) {
for (final link in links) {
if (link.type?.startsWith(type) ?? false) {
return link;
}
}
}
return null;
}

@override
Future<OpenAPIDocument> 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<OGCFeatureConformance> conformance() async {
// fetch data as JSON Object, and parse conformance classes
Expand Down

0 comments on commit eb10788

Please sign in to comment.