Skip to content

Commit

Permalink
Complete adding property filters to OpenAPI
Browse files Browse the repository at this point in the history
  • Loading branch information
rkettelerij committed Dec 22, 2023
1 parent 88b4bae commit 76d669e
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 55 deletions.
22 changes: 22 additions & 0 deletions engine/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ type CollectionEntryFeatures struct {

// Optional collection specific datasources. Mutually exclusive with top-level defined datasources.
Datasources *Datasources `yaml:"datasources"`

Filters struct {
// OAF Part 1: filter on feature properties
Properties []PropertyFilter `yaml:"properties" validate:"dive"`

// OAF Part 3: add config for complex/CQL filters here
// placeholder
} `yaml:"filters"`
}

type CollectionEntryMaps struct {
Expand Down Expand Up @@ -311,6 +319,15 @@ func (oaf OgcAPIFeatures) ProjectionsForCollection(collectionID string) []string
return result
}

func (oaf OgcAPIFeatures) PropertyFiltersForCollection(collectionID string) ([]PropertyFilter, error) {
for _, coll := range oaf.Collections {
if coll.ID == collectionID && coll.Features != nil && coll.Features.Filters.Properties != nil {
return coll.Features.Filters.Properties, nil
}
}
return nil, errors.New("not found")
}

type OgcAPIMaps struct {
Collections GeoSpatialCollections `yaml:"collections"`
}
Expand Down Expand Up @@ -394,6 +411,11 @@ type GeoPackageCloud struct {
Cache *string `yaml:"cache" validate:"omitempty,dir"`
}

type PropertyFilter struct {
Name string `yaml:"name" validate:"required"`
Description string `yaml:"description"`
}

type SupportedSrs struct {
Srs string `yaml:"srs" validate:"required,startswith=EPSG:"`
ZoomLevelRange ZoomLevelRange `yaml:"zoomLevelRange" validate:"required"`
Expand Down
8 changes: 4 additions & 4 deletions engine/templates/openapi/features.go.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@
}
{{ if and $.Params $.Params.PropertyFiltersByCollection }}
{{- range $pf := $.Params.PropertyFiltersByCollection -}}
{{- range $propFilterName, $propFilterType := $pf -}}
{{- range $propFilter := $pf -}}
,{
"name": "{{ $propFilterName }}",
"name": "{{ $propFilter.Name }}",
"in": "query",
"description": "The optional blaat.",
"description": "{{ $propFilter.Description }}",
"required": false,
"style": "form",
"explode": false,
"schema": {
"type": "{{ $propFilterType }}"
"type": "{{ $propFilter.DataType }}"
}
}
{{ end }}
Expand Down
69 changes: 18 additions & 51 deletions ogc/features/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,18 @@ type Features struct {
json *jsonFeatures
}

type PropertyNamesWithType map[string]string

func NewFeatures(e *engine.Engine, router *chi.Mux) *Features {
collections = cacheCollectionsMetadata(e)
datasources := configureDatasources(e)

rebuildOpenAPIForFeatures(e, datasources)

f := &Features{
engine: e,
datasources: configureDatasources(e),
datasources: datasources,
html: newHTMLFeatures(e),
json: newJSONFeatures(e),
}
collections = f.cacheCollectionsMetadata()

// Rebuild OpenAPI spec with additional info from datasources
e.RebuildOpenAPI(struct {
PropertyFiltersByCollection map[string]PropertyNamesWithType
}{
PropertyFiltersByCollection: f.createPropertyFiltersByCollection(),
})

router.Get(geospatial.CollectionsPath+"/{collectionId}/items", f.CollectionContent())
router.Get(geospatial.CollectionsPath+"/{collectionId}/items/{featureId}", f.Feature())
Expand All @@ -77,6 +72,11 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc {
}

collectionID := chi.URLParam(r, "collectionId")
if _, ok := collections[collectionID]; !ok {
log.Printf("collection %s doesn't exist in this features service", collectionID)
http.NotFound(w, r)
return
}
url := featureCollectionURL{*cfg.BaseURL.URL, r.URL.Query(), cfg.OgcAPI.Features.Limit}
encodedCursor, limit, inputSRID, outputSRID, contentCrs, bbox, err := url.parse()
if err != nil {
Expand All @@ -87,11 +87,6 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if _, ok := collections[collectionID]; !ok {
log.Printf("collection %s doesn't exist in this features service", collectionID)
http.NotFound(w, r)
return
}
w.Header().Add(engine.HeaderContentCrs, contentCrs.ToLink())

var newCursor domain.Cursors
Expand Down Expand Up @@ -165,12 +160,16 @@ func (f *Features) Feature() http.HandlerFunc {
}

collectionID := chi.URLParam(r, "collectionId")
if _, ok := collections[collectionID]; !ok {
log.Printf("collection %s doesn't exist in this features service", collectionID)
http.NotFound(w, r)
return
}
featureID, err := strconv.Atoi(chi.URLParam(r, "featureId"))
if err != nil {
http.Error(w, "feature ID must be a number", http.StatusBadRequest)
return
}

url := featureURL{*f.engine.Config.BaseURL.URL, r.URL.Query()}
outputSRID, contentCrs, err := url.parse()
if err != nil {
Expand All @@ -181,11 +180,6 @@ func (f *Features) Feature() http.HandlerFunc {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if _, ok := collections[collectionID]; !ok {
log.Printf("collection %s doesn't exist in this features service", collectionID)
http.NotFound(w, r)
return
}
w.Header().Add(engine.HeaderContentCrs, contentCrs.ToLink())

datasource := f.datasources[DatasourceKey{srid: outputSRID.GetOrDefault(), collectionID: collectionID}]
Expand Down Expand Up @@ -218,41 +212,14 @@ func (f *Features) Feature() http.HandlerFunc {
}
}

func (f *Features) cacheCollectionsMetadata() map[string]*engine.GeoSpatialCollectionMetadata {
func cacheCollectionsMetadata(e *engine.Engine) map[string]*engine.GeoSpatialCollectionMetadata {
result := make(map[string]*engine.GeoSpatialCollectionMetadata)
for _, collection := range f.engine.Config.OgcAPI.Features.Collections {
for _, collection := range e.Config.OgcAPI.Features.Collections {
result[collection.ID] = collection.Metadata
}
return result
}

func (f *Features) createPropertyFiltersByCollection() map[string]PropertyNamesWithType {
propertyFiltersByCollection := make(map[string]PropertyNamesWithType)
for k, v := range f.datasources {
metadata, err := v.GetFeatureTableMetadata(k.collectionID)
if err != nil {
continue
}
propertyFilters := make(PropertyNamesWithType)
for name, dataType := range metadata.ColumnsWithDataType() {
// translate database data types to OpenAPI data types
switch dataType {
case "INTEGER":
dataType = "integer"
case "REAL":
dataType = "number"
case "TEXT":
dataType = "string"
default:
dataType = "string"
}
propertyFilters[name] = dataType
}
propertyFiltersByCollection[k.collectionID] = propertyFilters
}
return propertyFiltersByCollection
}

func configureDatasources(e *engine.Engine) map[DatasourceKey]ds.Datasource {
result := make(map[DatasourceKey]ds.Datasource, len(e.Config.OgcAPI.Features.Collections))

Expand Down
72 changes: 72 additions & 0 deletions ogc/features/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package features

import (
"strings"

"github.com/PDOK/gokoala/engine"
ds "github.com/PDOK/gokoala/ogc/features/datasources"
)

type OpenAPIPropertyFilter struct {
Name string
Description string
DataType string
}

// rebuildOpenAPIForFeatures Rebuild OpenAPI spec with additional info from given datasources
func rebuildOpenAPIForFeatures(e *engine.Engine, datasources map[DatasourceKey]ds.Datasource) {
e.RebuildOpenAPI(struct {
PropertyFiltersByCollection map[string][]OpenAPIPropertyFilter
}{
PropertyFiltersByCollection: createPropertyFiltersByCollection(e.Config.OgcAPI.Features, datasources),
})
}

func createPropertyFiltersByCollection(
oafConfig *engine.OgcAPIFeatures,
datasources map[DatasourceKey]ds.Datasource) map[string][]OpenAPIPropertyFilter {

result := make(map[string][]OpenAPIPropertyFilter)
for k, datasource := range datasources {
filtersConfig, err := oafConfig.PropertyFiltersForCollection(k.collectionID)
if err != nil {
continue
}
featTable, err := datasource.GetFeatureTableMetadata(k.collectionID)
if err != nil {
continue
}
featTableColumns := featTable.ColumnsWithDataType()
propertyFilters := make([]OpenAPIPropertyFilter, 0, len(featTableColumns))
for name, dataType := range featTableColumns {
for _, fc := range filtersConfig {
if fc.Name == name {
dataType = datasourceToOpenAPI(dataType)
propertyFilters = append(propertyFilters, OpenAPIPropertyFilter{
Name: name,
Description: fc.Description,
DataType: dataType,
})
}
}

}
result[k.collectionID] = propertyFilters
}
return result
}

// translate database data types to OpenAPI data types
func datasourceToOpenAPI(dataType string) string {
switch strings.ToUpper(dataType) {
case "INTEGER":
dataType = "integer"
case "REAL", "NUMERIC":
dataType = "number"
case "TEXT", "VARCHAR":
dataType = "string"
default:
dataType = "string"
}
return dataType
}

0 comments on commit 76d669e

Please sign in to comment.