Skip to content

Commit d9c8230

Browse files
feat(CREATE): add openapi for /collections/{collectionId}/items POST
* openapi.go: add paramFeatureID, FeatureSchema, getFeatureExample()
1 parent b9cf333 commit d9c8230

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

internal/api/net.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const (
4444
// ContentTypeHTML
4545
ContentTypeOpenAPI = "application/vnd.oai.openapi+json;version=3.0"
4646

47+
// ContentTypeAny
48+
ContentTypeAny = "*/*"
49+
4750
// FormatJSON code and extension for JSON
4851
FormatJSON = "json"
4952

@@ -61,6 +64,9 @@ const (
6164

6265
// FormatXML code and extension for XML/GML
6366
FormatXML = "xml"
67+
68+
// FormatAny code and extension for */*
69+
FormatAny = "any"
6470
)
6571

6672
// RequestedFormat gets the format for a request from extension or headers
@@ -85,6 +91,9 @@ func RequestedFormat(r *http.Request) string {
8591
if strings.Contains(hdrAccept, ContentTypeSchemaJSON) {
8692
return FormatSchemaJSON
8793
}
94+
if strings.Contains(hdrAccept, ContentTypeAny) {
95+
return FormatAny
96+
}
8897
if strings.Contains(hdrAccept, ContentTypeHTML) {
8998
return FormatHTML
9099
}

internal/api/openapi.go

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,80 @@ package api
1414
*/
1515

1616
import (
17+
"encoding/json"
1718
"net/url"
1819

1920
"github.com/CrunchyData/pg_featureserv/internal/conf"
2021
"github.com/getkin/kin-openapi/openapi3"
2122
log "github.com/sirupsen/logrus"
2223
)
2324

25+
func getFeatureExample() map[string]interface{} {
26+
var result map[string]interface{}
27+
var jsonStr = `{"type":"Feature","geometry":{"type":"Point","coordinates":[-70.88461956597838,47.807897059236495]},"properties":{"prop_a":"propA","prop_b":1,"prop_c":"propC","prop_d":1}}`
28+
err := json.Unmarshal([]byte(jsonStr), &result)
29+
if err != nil {
30+
return nil
31+
}
32+
return result
33+
}
34+
35+
var FeatureSchema openapi3.Schema = openapi3.Schema{
36+
Type: "object",
37+
Required: []string{},
38+
Properties: map[string]*openapi3.SchemaRef{
39+
"id": {
40+
Value: &openapi3.Schema{
41+
OneOf: []*openapi3.SchemaRef{
42+
openapi3.NewSchemaRef("", &openapi3.Schema{
43+
Type: "number", Format: "long",
44+
}),
45+
openapi3.NewSchemaRef("", &openapi3.Schema{
46+
Type: "string",
47+
}),
48+
},
49+
},
50+
},
51+
"type": {
52+
Value: &openapi3.Schema{
53+
Type: "string",
54+
Default: "Feature",
55+
},
56+
},
57+
"geometry": {
58+
Value: &openapi3.Schema{
59+
Items: &openapi3.SchemaRef{
60+
Value: &openapi3.Schema{
61+
Type: "string", // mandatory to validate the schema
62+
OneOf: []*openapi3.SchemaRef{
63+
openapi3.NewSchemaRef("https://geojson.org/schema/Point.json", &openapi3.Schema{Type: "string"}),
64+
openapi3.NewSchemaRef("https://geojson.org/schema/LineString.json", &openapi3.Schema{Type: "string"}),
65+
openapi3.NewSchemaRef("https://geojson.org/schema/Polygon.json", &openapi3.Schema{Type: "string"}),
66+
openapi3.NewSchemaRef("https://geojson.org/schema/MultiPoint.json", &openapi3.Schema{Type: "string"}),
67+
openapi3.NewSchemaRef("https://geojson.org/schema/MultiLineString.json", &openapi3.Schema{Type: "string"}),
68+
openapi3.NewSchemaRef("https://geojson.org/schema/MultiPolygon.json", &openapi3.Schema{Type: "string"}),
69+
},
70+
},
71+
},
72+
},
73+
},
74+
"properties": {
75+
Value: &openapi3.Schema{
76+
Type: "object",
77+
},
78+
},
79+
"bbox": {
80+
Value: &openapi3.Schema{
81+
Type: "array",
82+
MinItems: 4,
83+
MaxItems: openapi3.Uint64Ptr(4),
84+
Items: openapi3.NewSchemaRef("", openapi3.NewFloat64Schema().WithMin(-180).WithMax(180)),
85+
},
86+
},
87+
},
88+
Example: getFeatureExample(),
89+
}
90+
2491
// GetOpenAPIContent returns a Swagger OpenAPI structure
2592
func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
2693

@@ -53,6 +120,16 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
53120
AllowEmptyValue: false,
54121
},
55122
}
123+
paramFeatureID := openapi3.ParameterRef{
124+
Value: &openapi3.Parameter{
125+
Name: "featureId",
126+
Description: "Id of feature in collection to retrieve data for.",
127+
In: "path",
128+
Required: true,
129+
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
130+
AllowEmptyValue: false,
131+
},
132+
}
56133
paramBbox := openapi3.ParameterRef{
57134
Value: &openapi3.Parameter{
58135
Name: "bbox",
@@ -216,7 +293,7 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
216293
Name: "type",
217294
Description: "Data schema type (create, update, etc.).",
218295
In: "query",
219-
Required: false,
296+
Required: true,
220297
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
221298
AllowEmptyValue: false,
222299
},
@@ -365,6 +442,35 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
365442
},
366443
},
367444
},
445+
Post: &openapi3.Operation{
446+
OperationID: "createCollectionFeature",
447+
Parameters: openapi3.Parameters{
448+
&paramCollectionID,
449+
// TODO keep it for the next evolution
450+
// &paramCrs,
451+
},
452+
RequestBody: &openapi3.RequestBodyRef{
453+
Value: &openapi3.RequestBody{
454+
Description: "Add a new feature",
455+
Required: true,
456+
Content: openapi3.NewContentWithJSONSchema(&FeatureSchema),
457+
},
458+
},
459+
Responses: openapi3.Responses{
460+
"201": &openapi3.ResponseRef{
461+
Value: &openapi3.Response{
462+
Description: "Empty body with location header",
463+
Headers: map[string]*openapi3.HeaderRef{
464+
"location": {
465+
Value: &openapi3.Header{
466+
Description: "Contains a link to access to the new feature data",
467+
},
468+
},
469+
},
470+
},
471+
},
472+
},
473+
},
368474
},
369475
apiBase + "collections/{collectionId}/schema": &openapi3.PathItem{
370476
Summary: "Feature schema for collection",
@@ -374,6 +480,16 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
374480
Parameters: openapi3.Parameters{
375481
&paramCollectionID,
376482
&paramType,
483+
&openapi3.ParameterRef{
484+
Value: &openapi3.Parameter{
485+
Name: "accept",
486+
Description: "Only accept application/schema+schema",
487+
In: "header",
488+
Required: true,
489+
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
490+
AllowEmptyValue: false,
491+
},
492+
},
377493
},
378494
Responses: openapi3.Responses{
379495
"200": &openapi3.ResponseRef{
@@ -399,16 +515,7 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger {
399515
OperationID: "getCollectionFeature",
400516
Parameters: openapi3.Parameters{
401517
&paramCollectionID,
402-
&openapi3.ParameterRef{
403-
Value: &openapi3.Parameter{
404-
Name: "featureId",
405-
Description: "Id of feature in collection to retrieve data for.",
406-
In: "path",
407-
Required: true,
408-
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
409-
AllowEmptyValue: false,
410-
},
411-
},
518+
&paramFeatureID,
412519
&paramProperties,
413520
&paramTransform,
414521
&paramCrs,

internal/service/handler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,9 @@ func handleCollectionSchemas(w http.ResponseWriter, r *http.Request) *appError {
286286

287287
ctx := r.Context()
288288
switch format {
289-
case api.FormatSchemaJSON:
289+
case api.FormatSchemaJSON,
290+
// workaround to have swagger working:
291+
api.FormatAny:
290292
{
291293
switch schemaType {
292294
case "create":

internal/service/handler_post_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,23 @@ func TestApiContainsCollectionSchemas(t *testing.T) {
4242
util.Assert(t, path != nil, "schema path exists")
4343
util.Equals(t, "Provides access to data representation (schema) for any features in specified collection", path.Description, "schema path present")
4444
util.Equals(t, "getCollectionSchema", path.Get.OperationID, "schema path get present")
45-
util.Equals(t, 2, len(path.Get.Parameters), "schema path get present")
45+
util.Equals(t, 2, len(path.Get.Parameters), "# path")
4646
util.Assert(t, path.Get.Parameters.GetByInAndName("path", "collectionId") != nil, "collectionId path parameter exists")
4747
util.Assert(t, path.Get.Parameters.GetByInAndName("query", "type") != nil, "type query parameter exists")
4848
}
4949

50+
// checks swagger api contains method PATCH for updating a feaure from a specified collection
51+
func TestApiContainsMethodPostFeature(t *testing.T) {
52+
resp := hTest.DoRequest(t, "/api")
53+
body, _ := ioutil.ReadAll(resp.Body)
54+
55+
var v openapi3.Swagger
56+
err := json.Unmarshal(body, &v)
57+
util.Assert(t, err == nil, fmt.Sprintf("%v", err))
58+
59+
util.Equals(t, "createCollectionFeature", v.Paths.Find("/collections/{collectionId}/items").Post.OperationID, "method POST present")
60+
}
61+
5062
// checks collection schema contains valid data description
5163
func TestGetCollectionCreateSchema(t *testing.T) {
5264
path := "/collections/mock_a/schema?type=create"

0 commit comments

Comments
 (0)