Skip to content

Commit 9b92225

Browse files
committed
feat: schema server
1 parent 3277316 commit 9b92225

6 files changed

Lines changed: 186 additions & 24 deletions

File tree

.github/workflows/docker.yaml

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,32 @@ jobs:
2020
matrix:
2121
variant:
2222
- name: public
23-
config_branch: main
24-
suffix: "-readonly"
25-
edit: "true"
23+
image_suffix: ""
24+
tag_suffix: "-readonly"
25+
target: ""
26+
build_args: |
27+
VITE_DEFAULT_CONFIGURATION=https://s3-ext-1.nina.no/dms/maps/main/maps.json
28+
VITE_HIDE_EDIT_BUTTON=true
2629
- name: internal
27-
config_branch: main
28-
suffix: ""
29-
edit: "false"
30+
image_suffix: ""
31+
tag_suffix: ""
32+
target: ""
33+
build_args: |
34+
VITE_DEFAULT_CONFIGURATION=https://s3-ext-1.nina.no/dms/maps/main/maps.json
35+
VITE_HIDE_EDIT_BUTTON=false
36+
- name: server
37+
image_suffix: "-server"
38+
tag_suffix: ""
39+
target: "server"
40+
build_args: ""
3041

3142
steps:
3243
- name: Checkout repository
3344
uses: actions/checkout@v3
3445
with:
3546
persist-credentials: false
47+
- name: Setup Buildx
48+
uses: docker/setup-buildx-action@v3
3649
- name: Login to registry
3750
if: github.event_name != 'pull_request'
3851
uses: docker/login-action@v2
@@ -44,17 +57,19 @@ jobs:
4457
id: meta
4558
uses: docker/metadata-action@v4
4659
with:
47-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
60+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.image_suffix }}
4861
tags: |
49-
type=ref,event=branch,suffix=${{ matrix.variant.suffix }}
50-
type=sha,suffix=${{ matrix.variant.suffix }}
62+
type=ref,event=branch,suffix=${{ matrix.variant.tag_suffix }}
63+
type=sha,suffix=${{ matrix.variant.tag_suffix }}
64+
type=semver,pattern={{version}},suffix=${{ matrix.variant.tag_suffix }}
5165
- name: Build and push Docker image
5266
uses: docker/build-push-action@v3
5367
with:
5468
context: .
55-
push: ${{ github.event_name != 'pull_request' }}
69+
target: ${{ matrix.variant.target }}
70+
push: ${{ github.event_name != 'pull_request' }}
5671
tags: ${{ steps.meta.outputs.tags }}
5772
labels: ${{ steps.meta.outputs.labels }}
58-
build-args: |
59-
VITE_DEFAULT_CONFIGURATION=https://s3-ext-1.nina.no/dms/maps/${{ matrix.variant.config_branch }}/maps.json
60-
VITE_HIDE_EDIT_BUTTON=${{ matrix.variant.edit }}
73+
build-args: ${{ matrix.variant.build_args }}
74+
cache-from: type=gha,scope=${{ matrix.variant.name }}
75+
cache-to: type=gha,mode=max,scope=${{ matrix.variant.name }}

Dockerfile

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,23 @@ EXPOSE 5173
2929
CMD ["pnpm", "run", "dev"]
3030

3131

32-
FROM nginx:stable-alpine-slim
32+
FROM nginx:stable-alpine-slim AS nginx
3333

3434
COPY nginx/default.conf.template /etc/nginx/templates/
3535
COPY --from=build /app/dist /var/www/
36+
37+
38+
FROM denoland/deno:2 AS server
39+
40+
WORKDIR /app
41+
42+
COPY deno.json package.json ./
43+
COPY src/server.ts src/map-config.ts src/
44+
COPY src/schemas/ src/schemas/
45+
46+
RUN deno cache --sloppy-imports src/server.ts
47+
48+
EXPOSE 8080
49+
ENV PORT=8080
50+
51+
CMD ["run", "--sloppy-imports", "--allow-net", "--allow-env=PORT", "src/server.ts"]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"generate-schema": "deno run --sloppy-imports src/cli.ts schema > schemas/map-config.schema.json",
1414
"cli": "deno run --sloppy-imports src/cli.ts",
1515
"cli:build": "deno compile --sloppy-imports --output dist/nina-map-cli src/cli.ts",
16+
"server": "deno run --sloppy-imports --allow-net --allow-env=PORT src/server.ts",
1617
"prepare": "husky"
1718
},
1819
"dependencies": {

src/cli.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Command } from 'jsr:@cliffy/command@^1';
44
import * as z from 'zod';
55
import pkg from '../package.json' with { type: 'json' };
6-
import { MapConfigSchema } from './schemas';
6+
import { buildSchema, validateConfig } from './map-config.ts';
77

88
await new Command()
99
.name('nina-maps')
@@ -32,7 +32,7 @@ await new Command()
3232
Deno.exit(1);
3333
}
3434

35-
const result = MapConfigSchema.safeParse(data);
35+
const result = validateConfig(data);
3636

3737
if (result.success) {
3838
console.log('Valid map configuration.');
@@ -46,14 +46,7 @@ await new Command()
4646
.command(
4747
'schema',
4848
new Command().description('Print the JSON Schema for a nina-maps configuration.').action(() => {
49-
const schema = {
50-
$schema: 'https://json-schema.org/draft/2020-12/schema',
51-
title: 'Nina Maps Configuration',
52-
description: 'Configuration schema for a nina-maps map.',
53-
version: pkg.version,
54-
...z.toJSONSchema(MapConfigSchema),
55-
};
56-
console.log(JSON.stringify(schema, null, 2));
49+
console.log(JSON.stringify(buildSchema(), null, 2));
5750
}),
5851
)
5952
.parse(Deno.args);

src/map-config.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as z from 'zod';
2+
import pkg from '../package.json' with { type: 'json' };
3+
import { MapConfigSchema } from './schemas';
4+
5+
export { MapConfigSchema };
6+
7+
export function buildSchema() {
8+
return {
9+
$schema: 'https://json-schema.org/draft/2020-12/schema',
10+
title: 'Nina Maps Configuration',
11+
description: 'Configuration schema for a nina-maps map.',
12+
version: pkg.version,
13+
...z.toJSONSchema(MapConfigSchema),
14+
};
15+
}
16+
17+
export function validateConfig(data: unknown) {
18+
return MapConfigSchema.safeParse(data);
19+
}
20+
21+
export function buildOpenApiSpec() {
22+
const mapConfigSchema = z.toJSONSchema(MapConfigSchema, { target: 'openapi3.1' });
23+
24+
return {
25+
openapi: '3.1.0',
26+
info: {
27+
title: 'Nina Maps API',
28+
description: 'Validation API for nina-maps configurations.',
29+
version: pkg.version,
30+
},
31+
paths: {
32+
'/api/validate': {
33+
post: {
34+
summary: 'Validate a map configuration',
35+
requestBody: {
36+
required: true,
37+
content: { 'application/json': { schema: mapConfigSchema } },
38+
},
39+
responses: {
40+
'200': {
41+
description: 'Configuration is valid',
42+
content: {
43+
'application/json': { schema: { type: 'object', properties: { valid: { type: 'boolean' } } } },
44+
},
45+
},
46+
'400': {
47+
description: 'Invalid JSON body',
48+
content: {
49+
'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } },
50+
},
51+
},
52+
'422': {
53+
description: 'Validation failed',
54+
content: {
55+
'application/json': {
56+
schema: {
57+
type: 'object',
58+
properties: {
59+
valid: { type: 'boolean' },
60+
errors: { type: 'object' },
61+
},
62+
},
63+
},
64+
},
65+
},
66+
},
67+
},
68+
},
69+
'/api/schema': {
70+
get: {
71+
summary: 'Get the JSON Schema for a map configuration',
72+
responses: {
73+
'200': {
74+
description: 'JSON Schema object',
75+
content: { 'application/json': { schema: { type: 'object' } } },
76+
},
77+
},
78+
},
79+
},
80+
},
81+
};
82+
}

src/server.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env -S deno run --allow-net --allow-env=PORT
2+
/// <reference lib="deno.ns" />
3+
import * as z from 'zod';
4+
import { buildOpenApiSpec, buildSchema, validateConfig } from './map-config.ts';
5+
6+
const port = Number(Deno.env.get('PORT') ?? 8080);
7+
8+
const SWAGGER_UI_HTML = `<!DOCTYPE html>
9+
<html lang="en">
10+
<head>
11+
<meta charset="utf-8" />
12+
<title>Nina Maps API</title>
13+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
14+
</head>
15+
<body>
16+
<div id="swagger-ui"></div>
17+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
18+
<script>
19+
SwaggerUIBundle({ url: '/api/openapi.json', dom_id: '#swagger-ui', deepLinking: true });
20+
</script>
21+
</body>
22+
</html>`;
23+
24+
Deno.serve({ port }, async req => {
25+
const { pathname } = new URL(req.url);
26+
27+
if (pathname === '/api/schema' && req.method === 'GET') {
28+
return Response.json(buildSchema());
29+
}
30+
31+
if (pathname === '/api/openapi.json' && req.method === 'GET') {
32+
return Response.json(buildOpenApiSpec());
33+
}
34+
35+
if (pathname === '/api/docs' && req.method === 'GET') {
36+
return new Response(SWAGGER_UI_HTML, { headers: { 'content-type': 'text/html; charset=utf-8' } });
37+
}
38+
39+
if (pathname === '/api/validate' && req.method === 'POST') {
40+
let data: unknown;
41+
try {
42+
data = await req.json();
43+
} catch {
44+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
45+
}
46+
47+
const result = validateConfig(data);
48+
if (result.success) {
49+
return Response.json({ valid: true });
50+
}
51+
return Response.json({ valid: false, errors: z.treeifyError(result.error) }, { status: 422 });
52+
}
53+
54+
return new Response('Not Found', { status: 404 });
55+
});

0 commit comments

Comments
 (0)