Second checkpoint
This commit is contained in:
parent
9cf791b789
commit
ddbf41dbeb
4
Makefile
4
Makefile
@ -42,10 +42,6 @@ fmt: ## Format the code
|
||||
lint: ## Run golangci-lint (requires golangci-lint installed)
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## Regenerate the OpenAPI (Swagger) spec from swag annotations
|
||||
go tool swag init -g cmd/gis/main.go --parseInternal --output docs
|
||||
|
||||
.PHONY: tidy
|
||||
tidy: ## Tidy go.mod / go.sum
|
||||
go mod tidy
|
||||
|
||||
14
README.md
14
README.md
@ -25,7 +25,7 @@ migrations/ embedded goose SQL migrations
|
||||
configs/ .env.example
|
||||
deployments/ docker-compose (postgres, minio, rabbitmq)
|
||||
build/package/ Dockerfile
|
||||
docs/ generated OpenAPI/Swagger spec (swaggo/swag)
|
||||
api/openapi.yaml OpenAPI 3.1.1 spec (embedded + served at /openapi.yaml)
|
||||
```
|
||||
|
||||
## Domain
|
||||
@ -105,14 +105,10 @@ Health: `GET /healthz` (liveness), `GET /readyz` (DB + S3 + RabbitMQ).
|
||||
|
||||
### HTTP API
|
||||
|
||||
The API is documented with [swaggo/swag](https://github.com/swaggo/swag)
|
||||
annotations on the handlers. The generated spec lives in `docs/` and is served
|
||||
as interactive **Swagger UI** at `/swagger/index.html` while the server runs.
|
||||
Regenerate after changing annotations:
|
||||
|
||||
```sh
|
||||
make docs # go tool swag init -g cmd/gis/main.go --parseInternal --output docs
|
||||
```
|
||||
The API is described by an **OpenAPI 3.1.1** spec at
|
||||
[`api/openapi.yaml`](api/openapi.yaml), embedded into the binary. While the
|
||||
server runs it is served at `/openapi.yaml`, with an interactive **Redoc** UI at
|
||||
`/docs`.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|----------------------------|--------------------------------------|
|
||||
|
||||
700
api/openapi.yaml
Normal file
700
api/openapi.yaml
Normal file
@ -0,0 +1,700 @@
|
||||
openapi: 3.1.1
|
||||
info:
|
||||
title: GIS API
|
||||
version: "1.0.0"
|
||||
summary: Hierarchical categories and geo datasets with async processing.
|
||||
description: |
|
||||
HTTP API for hierarchical categories and geo datasets (vector,
|
||||
vector_with_kato, raster).
|
||||
|
||||
Datasets are processed asynchronously after upload, dispatched by `file_type`:
|
||||
- `vector` — the attribute table is extracted into `properties`.
|
||||
- `raster` — converted to a Cloud-Optimized GeoTIFF; footprint `geometry`
|
||||
and `bbox` are derived from the raster extent.
|
||||
- `vector_with_kato` — columns are detected for selection; the client then
|
||||
submits a KATO/year mapping, unpivoted into observations.
|
||||
|
||||
Poll `GET /datasets/{id}/status` (optionally long-polling) to follow progress
|
||||
through the status lifecycle:
|
||||
`pending → processing|parsing → awaiting_mapping → extracting → ready`
|
||||
(or `failed`).
|
||||
license:
|
||||
name: Proprietary
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: Local development
|
||||
|
||||
tags:
|
||||
- name: Health
|
||||
description: Liveness and readiness probes
|
||||
- name: Categories
|
||||
description: Hierarchical dataset categories
|
||||
- name: Datasets
|
||||
description: Geo dataset upload, processing, and retrieval
|
||||
|
||||
paths:
|
||||
/healthz:
|
||||
get:
|
||||
tags: [Health]
|
||||
summary: Liveness probe
|
||||
responses:
|
||||
"200":
|
||||
description: Process is alive
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
|
||||
/readyz:
|
||||
get:
|
||||
tags: [Health]
|
||||
summary: Readiness probe
|
||||
description: Runs dependency checks (Postgres, S3/MinIO, RabbitMQ).
|
||||
responses:
|
||||
"200":
|
||||
description: All dependencies reachable
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Readiness" }
|
||||
"503":
|
||||
description: One or more dependencies unavailable
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Readiness" }
|
||||
|
||||
/categories:
|
||||
get:
|
||||
tags: [Categories]
|
||||
summary: List categories
|
||||
parameters:
|
||||
- name: parent_id
|
||||
in: query
|
||||
required: false
|
||||
description: Filter to the direct children of this category.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: Categories ordered by name
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Category" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
post:
|
||||
tags: [Categories]
|
||||
summary: Create a category
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CategoryInput" }
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Category" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"409": { $ref: "#/components/responses/Conflict" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
|
||||
/categories/{id}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
get:
|
||||
tags: [Categories]
|
||||
summary: Get a category
|
||||
responses:
|
||||
"200":
|
||||
description: The category
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Category" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
put:
|
||||
tags: [Categories]
|
||||
summary: Update a category
|
||||
description: |
|
||||
Updates name, description, and parent. The new parent must exist and must
|
||||
not create a cycle (a category cannot be its own ancestor).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CategoryInput" }
|
||||
responses:
|
||||
"200":
|
||||
description: Updated
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Category" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"409": { $ref: "#/components/responses/Conflict" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
delete:
|
||||
tags: [Categories]
|
||||
summary: Delete a category
|
||||
description: Fails with 409 if datasets or child categories still reference it.
|
||||
responses:
|
||||
"204": { description: Deleted }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"409": { $ref: "#/components/responses/Conflict" }
|
||||
|
||||
/datasets:
|
||||
get:
|
||||
tags: [Datasets]
|
||||
summary: List datasets (paginated summaries)
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PageParam"
|
||||
- $ref: "#/components/parameters/PageSizeParam"
|
||||
- name: category_id
|
||||
in: query
|
||||
required: false
|
||||
description: Filter to a category.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: A page of dataset summaries
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DatasetSummaryPage" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
post:
|
||||
tags: [Datasets]
|
||||
summary: Upload a dataset
|
||||
description: |
|
||||
Multipart upload, validated by `file_type`, extension, and a content
|
||||
magic-byte check, then processed asynchronously (poll
|
||||
`GET /datasets/{id}/status`).
|
||||
|
||||
Allowed extensions: `vector`/`vector_with_kato` = `.geojson`/`.gpkg`/`.zip`;
|
||||
`raster` = `.tif`/`.tiff`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file, file_type, category_id, code]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
description: The geo file.
|
||||
file_type:
|
||||
$ref: "#/components/schemas/FileType"
|
||||
category_id:
|
||||
type: string
|
||||
format: uuid
|
||||
code:
|
||||
type: string
|
||||
description: Business code.
|
||||
name:
|
||||
type: string
|
||||
description: Display name; defaults to the filename if omitted.
|
||||
description:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
meta:
|
||||
type: string
|
||||
description: Arbitrary user JSON (sent as a JSON string).
|
||||
automated:
|
||||
type: boolean
|
||||
responses:
|
||||
"201":
|
||||
description: Dataset created; processing enqueued
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Dataset" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
|
||||
/datasets/{id}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
get:
|
||||
tags: [Datasets]
|
||||
summary: Get a dataset
|
||||
description: Full dataset, including geometry as GeoJSON and bbox for rasters.
|
||||
responses:
|
||||
"200":
|
||||
description: The dataset
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Dataset" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
delete:
|
||||
tags: [Datasets]
|
||||
summary: Delete a dataset
|
||||
description: Removes the row and the stored object(s).
|
||||
responses:
|
||||
"204": { description: Deleted }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/datasets/{id}/status:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
get:
|
||||
tags: [Datasets]
|
||||
summary: Get processing status (supports long polling)
|
||||
description: |
|
||||
Returns the current status immediately. If `current` is supplied, the
|
||||
request is held until the status differs from it, or until `wait` seconds
|
||||
elapse (then the unchanged status is returned).
|
||||
parameters:
|
||||
- name: current
|
||||
in: query
|
||||
required: false
|
||||
description: The client's last-known status; enables long polling.
|
||||
schema: { $ref: "#/components/schemas/DatasetStatus" }
|
||||
- name: wait
|
||||
in: query
|
||||
required: false
|
||||
description: Max seconds to hold the request (default 25, max 60).
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 60
|
||||
default: 25
|
||||
responses:
|
||||
"200":
|
||||
description: The dataset's status
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DatasetStatusInfo" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/datasets/{id}/download:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
get:
|
||||
tags: [Datasets]
|
||||
summary: Download the original uploaded file
|
||||
responses:
|
||||
"200":
|
||||
description: The file stream
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/datasets/{id}/mapping:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
post:
|
||||
tags: [Datasets]
|
||||
summary: Set the KATO column and year mapping (vector_with_kato)
|
||||
description: |
|
||||
Valid only for `vector_with_kato` datasets in `awaiting_mapping` (or
|
||||
`ready`, to re-map). The KATO column and every mapped column must be among
|
||||
the dataset's detected `attribute_columns`. Moves the dataset to
|
||||
`extracting`; observations are produced asynchronously.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/MappingInput" }
|
||||
responses:
|
||||
"200":
|
||||
description: Mapping saved; extraction enqueued
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Dataset" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"409": { $ref: "#/components/responses/Conflict" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
|
||||
/datasets/{id}/observations:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdParam"
|
||||
get:
|
||||
tags: [Datasets]
|
||||
summary: List a dataset's observations (paginated)
|
||||
description: Long-format values unpivoted from a vector_with_kato dataset.
|
||||
parameters:
|
||||
- name: kato_code
|
||||
in: query
|
||||
required: false
|
||||
description: Filter to a single KATO code.
|
||||
schema:
|
||||
type: string
|
||||
- $ref: "#/components/parameters/PageParam"
|
||||
- $ref: "#/components/parameters/PageSizeParam"
|
||||
responses:
|
||||
"200":
|
||||
description: A page of observations
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ObservationPage" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
components:
|
||||
parameters:
|
||||
IdParam:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
PageParam:
|
||||
name: page
|
||||
in: query
|
||||
required: false
|
||||
description: 1-based page number.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
PageSizeParam:
|
||||
name: page_size
|
||||
in: query
|
||||
required: false
|
||||
description: Items per page (default 20, max 100).
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Malformed request (e.g. invalid UUID or query parameter)
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
Conflict:
|
||||
description: Operation conflicts with existing data (e.g. foreign key)
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
ValidationError:
|
||||
description: Input failed validation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- { $ref: "#/components/schemas/Error" }
|
||||
- { $ref: "#/components/schemas/ValidationErrors" }
|
||||
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
|
||||
ValidationErrors:
|
||||
type: object
|
||||
description: Per-field validation messages.
|
||||
properties:
|
||||
errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
Readiness:
|
||||
type: object
|
||||
properties:
|
||||
ready:
|
||||
type: boolean
|
||||
components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
FileType:
|
||||
type: string
|
||||
enum: [vector_with_kato, vector, raster]
|
||||
|
||||
DatasetStatus:
|
||||
type: string
|
||||
description: Dataset processing lifecycle status.
|
||||
enum: [pending, processing, parsing, awaiting_mapping, extracting, ready, failed]
|
||||
|
||||
Category:
|
||||
type: object
|
||||
required: [id, name, description, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
parent_id:
|
||||
type: [string, "null"]
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CategoryInput:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
parent_id:
|
||||
type: [string, "null"]
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
maxLength: 255
|
||||
description:
|
||||
type: string
|
||||
maxLength: 2000
|
||||
|
||||
AttributeColumn:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
samples:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
YearColumn:
|
||||
type: object
|
||||
required: [column, date]
|
||||
properties:
|
||||
column:
|
||||
type: string
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
|
||||
GeoJSONGeometry:
|
||||
type: [object, "null"]
|
||||
description: A GeoJSON geometry object (e.g. Point, Polygon).
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
coordinates: true
|
||||
|
||||
DatasetSummary:
|
||||
type: object
|
||||
required: [id, category_id, code, name, file_type, size_bytes, status, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
category_id:
|
||||
type: string
|
||||
format: uuid
|
||||
code:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: [string, "null"]
|
||||
unit:
|
||||
type: [string, "null"]
|
||||
file_type:
|
||||
$ref: "#/components/schemas/FileType"
|
||||
size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
status:
|
||||
$ref: "#/components/schemas/DatasetStatus"
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Dataset:
|
||||
type: object
|
||||
required:
|
||||
[id, category_id, code, name, filename, storage_key, file_type,
|
||||
size_bytes, content_type, automated, status, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
category_id:
|
||||
type: string
|
||||
format: uuid
|
||||
code:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: [string, "null"]
|
||||
unit:
|
||||
type: [string, "null"]
|
||||
filename:
|
||||
type: string
|
||||
storage_key:
|
||||
type: string
|
||||
cog_storage_key:
|
||||
type: [string, "null"]
|
||||
description: Set for rasters once the Cloud-Optimized GeoTIFF is produced.
|
||||
file_type:
|
||||
$ref: "#/components/schemas/FileType"
|
||||
size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
content_type:
|
||||
type: string
|
||||
properties:
|
||||
type: [array, "null"]
|
||||
description: Extracted attribute table (plain vector); rows of key/value.
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
meta:
|
||||
type: [object, "null"]
|
||||
description: Arbitrary user-supplied JSON.
|
||||
automated:
|
||||
type: boolean
|
||||
status:
|
||||
$ref: "#/components/schemas/DatasetStatus"
|
||||
attribute_columns:
|
||||
type: [array, "null"]
|
||||
description: Detected columns (vector_with_kato), for mapping.
|
||||
items:
|
||||
$ref: "#/components/schemas/AttributeColumn"
|
||||
kato_column:
|
||||
type: [string, "null"]
|
||||
year_columns:
|
||||
type: [array, "null"]
|
||||
items:
|
||||
$ref: "#/components/schemas/YearColumn"
|
||||
parse_error:
|
||||
type: [string, "null"]
|
||||
description: Failure reason when status is `failed`.
|
||||
geometry:
|
||||
$ref: "#/components/schemas/GeoJSONGeometry"
|
||||
bbox:
|
||||
type: array
|
||||
description: "[minX, minY, maxX, maxY]; present only for rasters."
|
||||
items:
|
||||
type: number
|
||||
minItems: 4
|
||||
maxItems: 4
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
MappingInput:
|
||||
type: object
|
||||
required: [kato_column, year_columns]
|
||||
properties:
|
||||
kato_column:
|
||||
type: string
|
||||
examples: ["като"]
|
||||
year_columns:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/components/schemas/YearColumn"
|
||||
examples:
|
||||
- kato_column: "като"
|
||||
year_columns:
|
||||
- { column: F_2023, date: "2023-01-01" }
|
||||
- { column: D_2025, date: "2025-01-01" }
|
||||
|
||||
Observation:
|
||||
type: object
|
||||
required: [id, dataset_id, kato_code, date]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
dataset_id:
|
||||
type: string
|
||||
format: uuid
|
||||
kato_code:
|
||||
type: string
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
value:
|
||||
type: [number, "null"]
|
||||
description: Numeric cell value, or null when non-numeric/empty.
|
||||
value_text:
|
||||
type: [string, "null"]
|
||||
description: Non-numeric cell value, or null.
|
||||
|
||||
DatasetStatusInfo:
|
||||
type: object
|
||||
required: [id, status]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
status:
|
||||
$ref: "#/components/schemas/DatasetStatus"
|
||||
parse_error:
|
||||
type: [string, "null"]
|
||||
|
||||
DatasetSummaryPage:
|
||||
type: object
|
||||
required: [data, page, page_size, total, total_pages]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/DatasetSummary"
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
|
||||
ObservationPage:
|
||||
type: object
|
||||
required: [data, page, page_size, total, total_pages]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Observation"
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
9
api/spec.go
Normal file
9
api/spec.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Package api embeds the OpenAPI 3.1.1 specification for the HTTP API.
|
||||
package api
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// Spec is the OpenAPI 3.1.1 document (YAML).
|
||||
//
|
||||
//go:embed openapi.yaml
|
||||
var Spec []byte
|
||||
31
internal/transport/http/docs.go
Normal file
31
internal/transport/http/docs.go
Normal file
@ -0,0 +1,31 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
// redocHTML renders the OpenAPI spec with Redoc (loaded from a CDN), which
|
||||
// supports OpenAPI 3.1.
|
||||
const redocHTML = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GIS API</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<style>body { margin: 0; padding: 0; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/openapi.yaml"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// openAPISpec serves the embedded OpenAPI 3.1.1 document.
|
||||
func (deps RouterDeps) openAPISpec(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml")
|
||||
_, _ = w.Write(deps.OpenAPISpec)
|
||||
}
|
||||
|
||||
// docsUI serves the Redoc documentation page.
|
||||
func docsUI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(redocHTML))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user