Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix JSON-FG pagination + numberReturned (+ extra tests) #103

Merged
merged 4 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,25 @@ func Test_newRouter(t *testing.T) {
wantBody string
}{
{
name: "multiple_ogc_apis_single_collection_json",
name: "Serve multiple OGC APIs for single collection in JSON",
configFile: "engine/testdata/config_multiple_ogc_apis_single_collection.yaml",
apiCall: "http://localhost:8180/collections/NewYork?f=json",
wantBody: "engine/testdata/expected_multiple_ogc_apis_single_collection.json",
},
{
name: "multiple_ogc_apis_single_collection_html",
name: "Serve multiple OGC APIs for single collection in HTML",
configFile: "engine/testdata/config_multiple_ogc_apis_single_collection.yaml",
apiCall: "http://localhost:8180/collections/NewYork?f=html",
wantBody: "engine/testdata/expected_multiple_ogc_apis_single_collection.html",
},
{
name: "ogc_api_processes",
name: "Serve multiple Feature Tables from single GeoPackage",
configFile: "ogc/features/testdata/config_features_bag_multiple_feature_tables.yaml",
apiCall: "http://localhost:8180/collections?f=json",
wantBody: "ogc/features/testdata/expected_multiple_feature_tables_single_geopackage.json",
},
{
name: "Check conformance of OGC API Processes",
configFile: "engine/testdata/config_processes.yaml",
apiCall: "http://localhost:8181/conformance?f=html",
wantBody: "engine/testdata/expected_processes_conformance.html",
Expand Down
6 changes: 3 additions & 3 deletions ogc/features/datasources/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// Datasource holds all Features for a single object type in a specific projection.
type Datasource interface {

// GetFeatureIDs returns all IDs of Features matching the given criteria, as well as and Cursors for pagination.
// GetFeatureIDs returns all IDs of Features matching the given criteria, as well as Cursors for pagination.
// To be used in concert with GetFeaturesByID
GetFeatureIDs(ctx context.Context, collection string, criteria FeaturesCriteria) ([]int64, domain.Cursors, error)

Expand Down Expand Up @@ -54,7 +54,7 @@ type FeaturesCriteria struct {
// FeatureTableMetadata abstraction to access metadata of a feature table (aka attribute table)
type FeatureTableMetadata interface {

// ColumnsWithDataType returns a mapping from colum names to column data types.
// Note: data types could be datasource specific.
// ColumnsWithDataType returns a mapping from column names to column data types.
// Note: data types can be datasource specific.
ColumnsWithDataType() map[string]string
}
10 changes: 5 additions & 5 deletions ogc/features/datasources/postgis/postgis.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,26 @@ func (PostGIS) Close() {
}

func (pg PostGIS) GetFeatureIDs(_ context.Context, _ string, _ datasources.FeaturesCriteria) ([]int64, domain.Cursors, error) {
log.Fatal("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
log.Println("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
return []int64{}, domain.Cursors{}, nil
}

func (pg PostGIS) GetFeaturesByID(_ context.Context, _ string, _ []int64) (*domain.FeatureCollection, error) {
log.Fatal("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
log.Println("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
return &domain.FeatureCollection{}, nil
}

func (pg PostGIS) GetFeatures(_ context.Context, _ string, _ datasources.FeaturesCriteria) (*domain.FeatureCollection, domain.Cursors, error) {
log.Fatal("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
log.Println("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
return nil, domain.Cursors{}, nil
}

func (pg PostGIS) GetFeature(_ context.Context, _ string, _ int64) (*domain.Feature, error) {
log.Fatal("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
log.Println("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
return nil, nil
}

func (pg PostGIS) GetFeatureTableMetadata(_ string) (datasources.FeatureTableMetadata, error) {
log.Fatal("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
log.Println("PostGIS support is not implemented yet, this just serves to demonstrate that we can support multiple types of datasources")
return nil, nil
}
46 changes: 46 additions & 0 deletions ogc/features/datasources/postgis/postgis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package postgis

import (
"context"
"testing"

"github.com/PDOK/gokoala/ogc/features/datasources"
"github.com/stretchr/testify/assert"
)

// PostGIS !!! Placeholder implementation, for future reference !!!
func TestPostGIS(t *testing.T) {
pg := PostGIS{}

t.Run("GetFeatureIDs", func(t *testing.T) {
ids, cursors, err := pg.GetFeatureIDs(context.Background(), "", datasources.FeaturesCriteria{})
assert.NoError(t, err)
assert.Empty(t, ids)
assert.NotNil(t, cursors)
})

t.Run("GetFeaturesByID", func(t *testing.T) {
fc, err := pg.GetFeaturesByID(context.Background(), "", nil)
assert.NoError(t, err)
assert.NotNil(t, fc)
})

t.Run("GetFeatures", func(t *testing.T) {
fc, cursors, err := pg.GetFeatures(context.Background(), "", datasources.FeaturesCriteria{})
assert.NoError(t, err)
assert.Nil(t, fc)
assert.NotNil(t, cursors)
})

t.Run("GetFeature", func(t *testing.T) {
f, err := pg.GetFeature(context.Background(), "", 0)
assert.NoError(t, err)
assert.Nil(t, f)
})

t.Run("GetFeatureTableMetadata", func(t *testing.T) {
metadata, err := pg.GetFeatureTableMetadata("")
assert.NoError(t, err)
assert.Nil(t, metadata)
})
}
4 changes: 2 additions & 2 deletions ogc/features/domain/jsonfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ type JSONFGFeatureCollection struct {
}

type JSONFGFeature struct {
// we overwrite ID since we want to make it a required attribute. We also expect feature ids to be
// auto-incrementing integers (which is the default in geopackages) since we use it for cursor-based pagination.
// We expect feature ids to be auto-incrementing integers (which is the default in geopackages)
// since we use it for cursor-based pagination.
ID int64 `json:"id"`
Links []Link `json:"links,omitempty"`
Type featureType `json:"type"`
Expand Down
56 changes: 40 additions & 16 deletions ogc/features/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (jf *jsonFeatures) featuresAsJSONFG(w http.ResponseWriter, r *http.Request,
fgFC.Features = append(fgFC.Features, &fgF)
}
}
fgFC.NumberReturned = fc.NumberReturned
fgFC.Timestamp = now().Format(time.RFC3339)
fgFC.Links = jf.createFeatureCollectionLinks(engine.FormatJSONFG, collectionID, cursor, featuresURL)

Expand Down Expand Up @@ -98,11 +99,11 @@ func (jf *jsonFeatures) featureAsJSONFG(w http.ResponseWriter, r *http.Request,
jf.engine.ServeResponse(w, r, false /* performed earlier */, engine.MediaTypeJSONFG, featJSON)
}

func (jf *jsonFeatures) createFeatureCollectionLinks(selfFormat string, collectionID string,
func (jf *jsonFeatures) createFeatureCollectionLinks(currentFormat string, collectionID string,
cursor domain.Cursors, featuresURL featureCollectionURL) []domain.Link {

links := make([]domain.Link, 0)
switch selfFormat {
switch currentFormat {
case engine.FormatGeoJSON:
links = append(links, domain.Link{
Rel: "self",
Expand Down Expand Up @@ -130,36 +131,59 @@ func (jf *jsonFeatures) createFeatureCollectionLinks(selfFormat string, collecti
Href: featuresURL.toSelfURL(collectionID, engine.FormatJSON),
})
}

links = append(links, domain.Link{
Rel: "alternate",
Title: "This document as HTML",
Type: engine.MediaTypeHTML,
Href: featuresURL.toSelfURL(collectionID, engine.FormatHTML),
})

if cursor.HasNext {
links = append(links, domain.Link{
Rel: "next",
Title: "Next page",
Type: engine.MediaTypeGeoJSON,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Next, engine.FormatJSON),
})
switch currentFormat {
rkettelerij marked this conversation as resolved.
Show resolved Hide resolved
case engine.FormatGeoJSON:
links = append(links, domain.Link{
Rel: "next",
Title: "Next page",
Type: engine.MediaTypeGeoJSON,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Next, engine.FormatJSON),
})
case engine.FormatJSONFG:
links = append(links, domain.Link{
Rel: "next",
Title: "Next page",
Type: engine.MediaTypeJSONFG,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Next, engine.FormatJSONFG),
})
}
}

if cursor.HasPrev {
links = append(links, domain.Link{
Rel: "prev",
Title: "Previous page",
Type: engine.MediaTypeGeoJSON,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Prev, engine.FormatJSON),
})
switch currentFormat {
case engine.FormatGeoJSON:
links = append(links, domain.Link{
Rel: "prev",
Title: "Previous page",
Type: engine.MediaTypeGeoJSON,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Prev, engine.FormatJSON),
})
case engine.FormatJSONFG:
links = append(links, domain.Link{
Rel: "prev",
Title: "Previous page",
Type: engine.MediaTypeJSONFG,
Href: featuresURL.toPrevNextURL(collectionID, cursor.Prev, engine.FormatJSONFG),
})
}
}
return links
}

func (jf *jsonFeatures) createFeatureLinks(selfFormat string, url featureURL,
func (jf *jsonFeatures) createFeatureLinks(currentFormat string, url featureURL,
collectionID string, featureID int64) []domain.Link {

links := make([]domain.Link, 0)
switch selfFormat {
switch currentFormat {
case engine.FormatGeoJSON:
links = append(links, domain.Link{
Rel: "self",
Expand Down
18 changes: 16 additions & 2 deletions ogc/features/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,20 @@ func TestFeatures_CollectionContent(t *testing.T) {
statusCode: http.StatusOK,
},
},
{
name: "Request output in default (WGS84) and bbox in default (WGS84) in JSOn-FG",
fields: fields{
configFile: "ogc/features/testdata/config_features_multiple_gpkgs.yaml",
url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86958187578342017%2C53.07965667574639212%2C4.88167082216529113%2C53.09197323827352477&cursor=Wl989YRHSw%3D%3D&f=jsonfg&limit=10",
collectionID: "dutch-addresses",
contentCrs: "<" + wgs84CrsURI + ">",
format: "json",
},
want: want{
body: "ogc/features/testdata/expected_multiple_gpkgs_bbox_wgs84_jsonfg.json",
statusCode: http.StatusOK,
},
},
{
name: "Request output in RD and bbox in default (WGS84)",
fields: fields{
Expand Down Expand Up @@ -535,7 +549,7 @@ func normalize(s string) string {
}

func printActual(rr *httptest.ResponseRecorder) {
log.Print("\n==> ACTUAL:")
log.Print("\n==> ACTUAL JSON RESPONSE (copy/paste and compare with response in file):")
log.Print(rr.Body.String()) // to ease debugging & updating expected results
log.Print("=========\n")
log.Print("\n=========\n")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
version: 1.0.2
title: OGC API Features
abstract: Contains a slimmed-down/example version of the BAG-dataset
baseUrl: http://localhost:8080
serviceIdentifier: Feats
license:
name: CC0
url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
ogcApi:
features:
datasources:
defaultWGS84:
geopackage:
local:
file: ./ogc/features/datasources/geopackage/testdata/bag.gpkg
fid: feature_id
queryTimeout: 15m # pretty high to allow debugging
collections:
- id: ligplaatsen
filters:
properties:
- name: straatnaam
- name: postcode
- id: standplaatsen
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"links": [
{
"rel": "self",
"type": "application/json",
"title": "This document as JSON",
"href": "http://localhost:8080/collections?f=json"
},
{
"rel": "alternate",
"type": "text/html",
"title": "This document as HTML",
"href": "http://localhost:8080/collections?f=html"
}
],
"collections": [
{
"id": "ligplaatsen",
"title": "ligplaatsen",
"crs": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84"
],
"storageCrs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"links": [
{
"rel": "self",
"type": "application/json",
"title": "Information about the ligplaatsen collection as JSON",
"href": "http://localhost:8080/collections/ligplaatsen?f=json"
},
{
"rel": "alternate",
"type": "text/html",
"title": "Information about the ligplaatsen collection as HTML",
"href": "http://localhost:8080/collections/ligplaatsen?f=json"
},
{
"rel": "items",
"type": "application/geo+json",
"title": "The JSON representation of the ligplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/ligplaatsen/items?f=json"
},
{
"rel": "items",
"type": "application/vnd.ogc.fg+json",
"title": "The JSON-FG representation of the ligplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/ligplaatsen/items?f=jsonfg"
},
{
"rel": "items",
"type": "text/html",
"title": "The HTML representation of the ligplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/ligplaatsen/items?f=html"
}
],
"content": []
},
{
"id": "standplaatsen",
"title": "standplaatsen",
"crs": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84"
],
"storageCrs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"links": [
{
"rel": "self",
"type": "application/json",
"title": "Information about the standplaatsen collection as JSON",
"href": "http://localhost:8080/collections/standplaatsen?f=json"
},
{
"rel": "alternate",
"type": "text/html",
"title": "Information about the standplaatsen collection as HTML",
"href": "http://localhost:8080/collections/standplaatsen?f=json"
},
{
"rel": "items",
"type": "application/geo+json",
"title": "The JSON representation of the standplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/standplaatsen/items?f=json"
},
{
"rel": "items",
"type": "application/vnd.ogc.fg+json",
"title": "The JSON-FG representation of the standplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/standplaatsen/items?f=jsonfg"
},
{
"rel": "items",
"type": "text/html",
"title": "The HTML representation of the standplaatsen features served from this endpoint",
"href": "http://localhost:8080/collections/standplaatsen/items?f=html"
}
],
"content": []
}
]
}
Loading
Loading