Skip to content

Commit 840ab89

Browse files
committed
added json-ld support
1 parent d3fd1df commit 840ab89

6 files changed

Lines changed: 187 additions & 1 deletion

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Added
2+
body: Added JSON-LD response with mandatory DCAT information to API detail
3+
time: 2026-04-28T18:30:18.398552372+02:00

api/openapi.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,11 @@
493493
"schema": {
494494
"$ref": "#/components/schemas/ApiDetail"
495495
}
496+
},
497+
"application/ld+json": {
498+
"schema": {
499+
"$ref": "#/components/schemas/ApiDetailJsonLd"
500+
}
496501
}
497502
}
498503
},
@@ -1335,6 +1340,85 @@
13351340
"type"
13361341
]
13371342
},
1343+
"ApiDetailJsonLd": {
1344+
"title": "API (JSON-LD)",
1345+
"description": "JSON-LD representation of an API as a dcat:DataService. Returned when Accept: application/ld+json is requested on /apis/{id}.",
1346+
"type": "object",
1347+
"properties": {
1348+
"@context": {
1349+
"type": "object",
1350+
"description": "Inline JSON-LD context defining the dcat, dct and vcard prefixes plus type-coercions for IRI-valued properties."
1351+
},
1352+
"@type": {
1353+
"type": "string",
1354+
"enum": [
1355+
"dcat:DataService"
1356+
]
1357+
},
1358+
"dct:conformsTo": {
1359+
"type": "array",
1360+
"items": {
1361+
"type": "string",
1362+
"format": "uri"
1363+
},
1364+
"examples": [
1365+
[
1366+
"https://spec.openapis.org/oas"
1367+
]
1368+
]
1369+
},
1370+
"dct:identifier": {
1371+
"type": "string",
1372+
"examples": [
1373+
"dicF0B3HR"
1374+
]
1375+
},
1376+
"dct:title": {
1377+
"type": "string"
1378+
},
1379+
"dct:description": {
1380+
"type": "string"
1381+
},
1382+
"dcat:endpointDescription": {
1383+
"type": "string",
1384+
"format": "uri",
1385+
"description": "URL of the OpenAPI document describing this DataService."
1386+
},
1387+
"dcat:contactPoint": {
1388+
"type": "object",
1389+
"properties": {
1390+
"vcard:fn": {
1391+
"type": "string"
1392+
},
1393+
"vcard:hasEmail": {
1394+
"type": "string",
1395+
"format": "uri",
1396+
"description": "mailto: IRI."
1397+
},
1398+
"vcard:hasURL": {
1399+
"type": "string",
1400+
"format": "uri"
1401+
}
1402+
},
1403+
"required": [
1404+
"vcard:fn"
1405+
]
1406+
},
1407+
"dct:publisher": {
1408+
"type": "string",
1409+
"format": "uri",
1410+
"description": "IRI of the publishing organisation."
1411+
}
1412+
},
1413+
"required": [
1414+
"@context",
1415+
"@type",
1416+
"dct:conformsTo",
1417+
"dct:identifier",
1418+
"dct:title",
1419+
"dcat:contactPoint"
1420+
]
1421+
},
13381422
"ApiInput": {
13391423
"title": "API Input",
13401424
"description": "Input for registering an API from its OpenAPI or Arazzo document",

pkg/api_client/handler/jsonld.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package handler
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"strings"
7+
8+
problem "github.com/developer-overheid-nl/don-api-register/pkg/api_client/helpers/problem"
9+
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/models"
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
const ApisJsonLdMediaType = "application/ld+json"
14+
15+
var apiDetailJsonLdContext = json.RawMessage(`{"dcat":"http://www.w3.org/ns/dcat#","dct":"http://purl.org/dc/terms/","vcard":"http://www.w3.org/2006/vcard/ns#","dcat:endpointDescription":{"@type":"@id"},"vcard:hasEmail":{"@type":"@id"},"vcard:hasURL":{"@type":"@id"},"dct:publisher":{"@type":"@id"}}`)
16+
17+
func oasConformsToURL(version string) string {
18+
v := strings.TrimSpace(version)
19+
if v == "" {
20+
return "https://spec.openapis.org/oas"
21+
}
22+
return "https://spec.openapis.org/oas/v" + v + ".html"
23+
}
24+
25+
// AcceptsJsonLd reports whether the Accept header explicitly requests application/ld+json.
26+
func AcceptsJsonLd(accept string) bool {
27+
for _, part := range strings.Split(accept, ",") {
28+
media := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
29+
if strings.EqualFold(media, ApisJsonLdMediaType) {
30+
return true
31+
}
32+
}
33+
return false
34+
}
35+
36+
// RetrieveApiJsonLd handles GET /apis/:id with Accept: application/ld+json.
37+
func (c *APIsAPIController) RetrieveApiJsonLd(ctx *gin.Context, params *models.ApiParams) error {
38+
api, err := c.Service.RetrieveApi(ctx.Request.Context(), params.Id)
39+
if err != nil {
40+
return err
41+
}
42+
if api == nil {
43+
return problem.NewNotFound(params.Id, "Api not found")
44+
}
45+
46+
contact := models.ContactJsonLd{FN: api.Contact.Name}
47+
if api.Contact.Email != "" {
48+
contact.HasEmail = "mailto:" + api.Contact.Email
49+
}
50+
if api.Contact.URL != "" {
51+
contact.HasURL = api.Contact.URL
52+
}
53+
54+
body := models.ApiDetailJsonLd{
55+
Context: apiDetailJsonLdContext,
56+
Type: "dcat:DataService",
57+
ConformsTo: []string{oasConformsToURL(api.OasVersion)},
58+
Identifier: api.Id,
59+
Title: api.Title,
60+
Description: api.Description,
61+
EndpointDescription: api.OasUrl,
62+
ContactPoint: contact,
63+
Publisher: api.Organisation.Uri,
64+
}
65+
data, err := json.Marshal(body)
66+
if err != nil {
67+
return err
68+
}
69+
ctx.Data(http.StatusOK, ApisJsonLdMediaType, data)
70+
return nil
71+
}

pkg/api_client/helpers/util/convert.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func ToApiDetail(api *models.Api) *models.ApiDetail {
5454
ApiSummary: ToApiSummary(api),
5555
DocsUrl: api.DocsUrl,
5656
Servers: servers,
57+
OasVersion: strings.TrimSpace(api.OAS.Version),
5758
}
5859
if auth := strings.TrimSpace(api.Auth); auth != "" {
5960
detail.Auth = []string{auth}

pkg/api_client/models/api.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ type ApiDetail struct {
180180
DocsUrl string `json:"docsUrl,omitempty"`
181181
Servers []ServerInfo `json:"servers,omitempty"`
182182
LintResults []LintResult `json:"lintResults,omitempty"`
183+
OasVersion string `json:"-"`
184+
}
185+
186+
type ContactJsonLd struct {
187+
FN string `json:"vcard:fn"`
188+
HasEmail string `json:"vcard:hasEmail,omitempty"`
189+
HasURL string `json:"vcard:hasURL,omitempty"`
190+
}
191+
192+
type ApiDetailJsonLd struct {
193+
Context json.RawMessage `json:"@context"`
194+
Type string `json:"@type"`
195+
ConformsTo []string `json:"dct:conformsTo"`
196+
Identifier string `json:"dct:identifier"`
197+
Title string `json:"dct:title"`
198+
Description string `json:"dct:description,omitempty"`
199+
EndpointDescription string `json:"dcat:endpointDescription,omitempty"`
200+
ContactPoint ContactJsonLd `json:"dcat:contactPoint"`
201+
Publisher string `json:"dct:publisher,omitempty"`
183202
}
184203

185204
type ApiPost struct {

pkg/api_client/routers.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ func NewRouter(apiVersion string, controller *handler.APIsAPIController) *fizz.F
108108
tonic.Handler(controller.ListApiFilters, 200),
109109
)
110110

111+
retrieveApiJson := tonic.Handler(controller.RetrieveApi, 200)
112+
retrieveApiJsonLd := tonic.Handler(controller.RetrieveApiJsonLd, 200)
111113
publicApis.GET("/apis/:id",
112114
[]fizz.OperationOption{
113115
fizz.ID("retreiveApi"),
@@ -123,7 +125,13 @@ func NewRouter(apiVersion string, controller *handler.APIsAPIController) *fizz.F
123125
apiVersionHeaderOption,
124126
notFoundResponse,
125127
},
126-
tonic.Handler(controller.RetrieveApi, 200),
128+
func(c *gin.Context) {
129+
if handler.AcceptsJsonLd(c.GetHeader("Accept")) {
130+
retrieveApiJsonLd(c)
131+
return
132+
}
133+
retrieveApiJson(c)
134+
},
127135
)
128136

129137
publicApis.GET("/apis/:id/postman",

0 commit comments

Comments
 (0)