From 83ab8c4cf93f6fc8f3941d8af340becf0a45280c Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Mon, 16 Mar 2026 03:15:56 +0100 Subject: [PATCH] Add OpenAPI/Swagger docs at /api/docs Full OpenAPI 3.0.3 spec with all 30+ endpoints, schemas, and query params. Swagger UI served from unpkg CDN at /api/docs, raw spec at /api/openapi.yaml. --- herbapi-api/openapi.yaml | 1990 +++++++++++++++++++++++++++++++++++ herbapi-api/src/api/docs.rs | 43 + 2 files changed, 2033 insertions(+) create mode 100644 herbapi-api/openapi.yaml create mode 100644 herbapi-api/src/api/docs.rs diff --git a/herbapi-api/openapi.yaml b/herbapi-api/openapi.yaml new file mode 100644 index 0000000..70c6da6 --- /dev/null +++ b/herbapi-api/openapi.yaml @@ -0,0 +1,1990 @@ +openapi: "3.0.3" +info: + title: HerbAPI + description: | + Plant reference database for permaculture and agroforestry planning. + Covers botanical families, species, cultivars, suppliers, companion planting + relationships, and images. + + **Authentication:** Write operations require an authenticated session via + OIDC (Authentik). Read endpoints are public. + version: "1.0.0" + contact: + name: Sub-Net e.U. + url: https://sub-net.at + email: florian.berthold@sub-net.at + +servers: + - url: / + description: Current instance + +tags: + - name: Health + description: Liveness probe + - name: Families + description: Botanical families (e.g. Fabaceae, Rosaceae) + - name: Species + description: Plant species within a family + - name: Cultivars + description: Named cultivars / varieties of a species + - name: Suppliers + description: Seed and plant suppliers + - name: Cultivar-Suppliers + description: Link cultivars to suppliers with pricing info + - name: Companions + description: Companion planting relationships between species + - name: Images + description: Entity images stored in S3 + - name: Search + description: Full-text search across all entities + - name: Auth + description: OIDC authentication (Authentik) + +paths: + # ── Health ────────────────────────────────────────────────────────── + /health: + get: + tags: [Health] + summary: Health check + operationId: health + responses: + "200": + description: Service is healthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ok + + # ── Families ──────────────────────────────────────────────────────── + /api/v1/families: + get: + tags: [Families] + summary: List families (paginated) + operationId: listFamilies + parameters: + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/search" + responses: + "200": + description: Paginated list of families + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedFamilies" + post: + tags: [Families] + summary: Create a family + operationId: createFamily + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateFamily" + responses: + "200": + description: Created family + content: + application/json: + schema: + $ref: "#/components/schemas/Family" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/families/{ref}: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Families] + summary: Get a family by slug or UUID + operationId: getFamily + responses: + "200": + description: Family details + content: + application/json: + schema: + $ref: "#/components/schemas/Family" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Families] + summary: Update a family + operationId: updateFamily + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateFamily" + responses: + "200": + description: Updated family + content: + application/json: + schema: + $ref: "#/components/schemas/Family" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Families] + summary: Delete a family (admin only) + operationId: deleteFamily + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + + # ── Species ───────────────────────────────────────────────────────── + /api/v1/species: + get: + tags: [Species] + summary: List species (paginated, filterable) + operationId: listSpecies + parameters: + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/search" + - name: family + in: query + description: Filter by family slug + schema: + type: string + - name: plant_layer + in: query + description: "Filter by plant layer (e.g. canopy, shrub, herb, ground_cover, climber, root)" + schema: + type: string + - name: nitrogen_fixer + in: query + description: Filter nitrogen-fixing species + schema: + type: boolean + - name: dynamic_accumulator + in: query + description: Filter dynamic accumulator species + schema: + type: boolean + - name: drought_tolerance + in: query + description: Filter by drought tolerance level + schema: + type: string + - name: native_status + in: query + description: Filter by native status + schema: + type: string + - name: min_nectar + in: query + description: Minimum nectar value (0-4) + schema: + type: integer + format: int16 + - name: min_bees + in: query + description: Minimum wild bee count + schema: + type: integer + format: int32 + responses: + "200": + description: Paginated list of species + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedSpecies" + post: + tags: [Species] + summary: Create a species + operationId: createSpecies + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSpecies" + responses: + "200": + description: Created species + content: + application/json: + schema: + $ref: "#/components/schemas/Species" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/species/{ref}: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Species] + summary: Get a species by slug or UUID + operationId: getSpecies + responses: + "200": + description: Species details + content: + application/json: + schema: + $ref: "#/components/schemas/Species" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Species] + summary: Update a species + operationId: updateSpecies + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSpecies" + responses: + "200": + description: Updated species + content: + application/json: + schema: + $ref: "#/components/schemas/Species" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Species] + summary: Delete a species (admin only) + operationId: deleteSpecies + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + + /api/v1/species/{ref}/companions: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Companions] + summary: List companion relationships for a species + operationId: listCompanionsForSpecies + responses: + "200": + description: List of companion relationships + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CompanionRelationship" + "404": + $ref: "#/components/responses/NotFound" + + # ── Cultivars ─────────────────────────────────────────────────────── + /api/v1/cultivars: + get: + tags: [Cultivars] + summary: List cultivars (paginated) + operationId: listCultivars + parameters: + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/search" + - name: species + in: query + description: Filter by species slug + schema: + type: string + responses: + "200": + description: Paginated list of cultivars + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedCultivars" + post: + tags: [Cultivars] + summary: Create a cultivar + operationId: createCultivar + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCultivar" + responses: + "200": + description: Created cultivar + content: + application/json: + schema: + $ref: "#/components/schemas/Cultivar" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/cultivars/{ref}: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Cultivars] + summary: Get a cultivar by slug or UUID + operationId: getCultivar + responses: + "200": + description: Cultivar details + content: + application/json: + schema: + $ref: "#/components/schemas/Cultivar" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Cultivars] + summary: Update a cultivar + operationId: updateCultivar + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCultivar" + responses: + "200": + description: Updated cultivar + content: + application/json: + schema: + $ref: "#/components/schemas/Cultivar" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Cultivars] + summary: Delete a cultivar (admin only) + operationId: deleteCultivar + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + + /api/v1/cultivars/{ref}/suppliers: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Cultivar-Suppliers] + summary: List suppliers linked to a cultivar + operationId: listCultivarSuppliers + responses: + "200": + description: List of cultivar-supplier links + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CultivarSupplier" + "404": + $ref: "#/components/responses/NotFound" + post: + tags: [Cultivar-Suppliers] + summary: Link a supplier to a cultivar + operationId: linkCultivarSupplier + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCultivarSupplier" + responses: + "200": + description: Created link + content: + application/json: + schema: + $ref: "#/components/schemas/CultivarSupplier" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/cultivars/{cid}/suppliers/{sid}: + parameters: + - name: cid + in: path + required: true + description: Cultivar UUID + schema: + type: string + format: uuid + - name: sid + in: path + required: true + description: Supplier UUID + schema: + type: string + format: uuid + delete: + tags: [Cultivar-Suppliers] + summary: Unlink a supplier from a cultivar + operationId: unlinkCultivarSupplier + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + + # ── Suppliers ─────────────────────────────────────────────────────── + /api/v1/suppliers: + get: + tags: [Suppliers] + summary: List all suppliers + operationId: listSuppliers + responses: + "200": + description: List of suppliers + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Supplier" + post: + tags: [Suppliers] + summary: Create a supplier + operationId: createSupplier + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSupplier" + responses: + "200": + description: Created supplier + content: + application/json: + schema: + $ref: "#/components/schemas/Supplier" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/suppliers/{ref}: + parameters: + - $ref: "#/components/parameters/ref" + get: + tags: [Suppliers] + summary: Get a supplier by slug or UUID + operationId: getSupplier + responses: + "200": + description: Supplier details + content: + application/json: + schema: + $ref: "#/components/schemas/Supplier" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Suppliers] + summary: Update a supplier + operationId: updateSupplier + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSupplier" + responses: + "200": + description: Updated supplier + content: + application/json: + schema: + $ref: "#/components/schemas/Supplier" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Suppliers] + summary: Delete a supplier (admin only) + operationId: deleteSupplier + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + + # ── Companions ────────────────────────────────────────────────────── + /api/v1/companions: + get: + tags: [Companions] + summary: List all companion relationships (with species names) + operationId: listCompanions + responses: + "200": + description: List of all companion relationships with resolved species names + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CompanionWithNames" + post: + tags: [Companions] + summary: Create a companion relationship + operationId: createCompanion + security: + - session: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCompanion" + responses: + "200": + description: Created relationship + content: + application/json: + schema: + $ref: "#/components/schemas/CompanionRelationship" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/companions/{id}: + parameters: + - name: id + in: path + required: true + description: Companion relationship UUID + schema: + type: string + format: uuid + delete: + tags: [Companions] + summary: Delete a companion relationship + operationId: deleteCompanion + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + + # ── Images ────────────────────────────────────────────────────────── + /api/v1/images/{entity_type}/{entity_id}: + parameters: + - name: entity_type + in: path + required: true + description: "Entity type (family, species, cultivar)" + schema: + type: string + enum: [family, species, cultivar] + - name: entity_id + in: path + required: true + description: Entity UUID + schema: + type: string + format: uuid + get: + tags: [Images] + summary: List images for an entity + operationId: listImagesForEntity + responses: + "200": + description: List of images + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Image" + + /api/v1/images: + post: + tags: [Images] + summary: Upload an image (multipart/form-data) + operationId: uploadImage + security: + - session: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [entity_type, entity_id, file] + properties: + entity_type: + type: string + enum: [family, species, cultivar] + entity_id: + type: string + format: uuid + file: + type: string + format: binary + caption: + type: string + source_url: + type: string + license: + type: string + is_primary: + type: string + enum: ["true", "false", "1", "0"] + responses: + "200": + description: Uploaded image metadata + content: + application/json: + schema: + $ref: "#/components/schemas/Image" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/images/{id}: + parameters: + - name: id + in: path + required: true + description: Image UUID + schema: + type: string + format: uuid + delete: + tags: [Images] + summary: Delete an image (admin only) + operationId: deleteImage + security: + - session: [] + responses: + "200": + $ref: "#/components/responses/Deleted" + "403": + $ref: "#/components/responses/Forbidden" + + /img/{path}: + parameters: + - name: path + in: path + required: true + description: S3 object key (e.g. species/uuid/image.jpg) + schema: + type: string + get: + tags: [Images] + summary: Serve an image from S3 + operationId: serveImage + responses: + "200": + description: Image binary data + content: + image/jpeg: + schema: + type: string + format: binary + image/png: + schema: + type: string + format: binary + image/webp: + schema: + type: string + format: binary + "404": + description: Image not found + + # ── Search ────────────────────────────────────────────────────────── + /api/v1/search: + get: + tags: [Search] + summary: Full-text search across families, species and cultivars + operationId: search + parameters: + - name: q + in: query + required: true + description: Search query (whitespace-separated terms, ANDed) + schema: + type: string + - name: limit + in: query + description: Maximum results (default 20, max 100) + schema: + type: integer + default: 20 + maximum: 100 + responses: + "200": + description: Search results ranked by relevance + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SearchResult" + + # ── Auth ──────────────────────────────────────────────────────────── + /auth/oidc/login: + get: + tags: [Auth] + summary: Initiate OIDC login (redirects to Authentik) + operationId: oidcLogin + responses: + "302": + description: Redirect to Authentik authorization endpoint + + /auth/oidc/callback: + get: + tags: [Auth] + summary: OIDC callback (handles code exchange) + operationId: oidcCallback + parameters: + - name: code + in: query + required: true + schema: + type: string + - name: state + in: query + required: true + schema: + type: string + responses: + "302": + description: Redirect to / after successful login + + /auth/oidc/logout: + get: + tags: [Auth] + summary: Logout (destroys session) + operationId: oidcLogout + responses: + "302": + description: Redirect to / + + /auth/me: + get: + tags: [Auth] + summary: Get current user info + operationId: me + responses: + "200": + description: Current authenticated user + content: + application/json: + schema: + $ref: "#/components/schemas/MeResponse" + "401": + description: Not authenticated + +components: + securitySchemes: + session: + type: apiKey + in: cookie + name: id + description: Session cookie set after OIDC login + + parameters: + page: + name: page + in: query + description: Page number (1-based, default 1) + schema: + type: integer + default: 1 + minimum: 1 + per_page: + name: per_page + in: query + description: Items per page (default 25, max 100) + schema: + type: integer + default: 25 + maximum: 100 + search: + name: search + in: query + description: Full-text search query + schema: + type: string + ref: + name: ref + in: path + required: true + description: Slug or UUID + schema: + type: string + + responses: + NotFound: + description: Entity not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + Forbidden: + description: Insufficient permissions + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + Deleted: + description: Successfully deleted + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + example: true + + schemas: + Error: + type: object + properties: + error: + type: string + required: [error] + + # ── Family ────────────────────────────────────────────────────── + Family: + type: object + properties: + id: + type: string + format: uuid + slug: + type: string + name_scientific: + type: string + name_en: + type: string + nullable: true + name_de: + type: string + nullable: true + description: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CreateFamily: + type: object + required: [name_scientific] + properties: + name_scientific: + type: string + name_en: + type: string + name_de: + type: string + description: + type: string + + UpdateFamily: + type: object + properties: + name_scientific: + type: string + name_en: + type: string + name_de: + type: string + description: + type: string + + PaginatedFamilies: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Family" + total: + type: integer + page: + type: integer + per_page: + type: integer + + # ── Species ───────────────────────────────────────────────────── + Species: + type: object + description: Full species record with all ecological and agronomic fields + properties: + id: + type: string + format: uuid + slug: + type: string + family_id: + type: string + format: uuid + name_scientific: + type: string + name_en: + type: string + nullable: true + name_de: + type: string + nullable: true + description: + type: string + nullable: true + description_de: + type: string + nullable: true + description_en: + type: string + nullable: true + soil_moisture: + type: string + nullable: true + drainage_requirement: + type: string + nullable: true + organic_matter_pct: + type: number + nullable: true + nitrogen_ppm: + type: integer + nullable: true + phosphorus_ppm: + type: integer + nullable: true + potassium_ppm: + type: integer + nullable: true + boron_ppm: + type: number + nullable: true + calcium_ppm: + type: integer + nullable: true + copper_ppm: + type: number + nullable: true + iron_ppm: + type: number + nullable: true + magnesium_ppm: + type: integer + nullable: true + manganese_ppm: + type: number + nullable: true + molybdenum_ppm: + type: number + nullable: true + sulfur_ppm: + type: integer + nullable: true + zinc_ppm: + type: number + nullable: true + ph_min: + type: number + nullable: true + ph_max: + type: number + nullable: true + soil_texture_preference: + type: array + items: + type: string + nullable: true + hardiness_zone_usda: + type: string + nullable: true + hardiness_zone_at: + type: string + nullable: true + min_temp: + type: number + nullable: true + max_temp: + type: number + nullable: true + drought_tolerance: + type: string + nullable: true + water_requirement_mm_week: + type: number + nullable: true + waterlogging_tolerance: + type: boolean + nullable: true + salt_tolerance: + type: string + nullable: true + edibility_rating: + type: integer + nullable: true + food_uses: + type: string + nullable: true + food_uses_de: + type: string + nullable: true + food_uses_en: + type: string + nullable: true + medicinal_uses: + type: string + nullable: true + medicinal_uses_de: + type: string + nullable: true + medicinal_uses_en: + type: string + nullable: true + other_uses: + type: string + nullable: true + other_uses_de: + type: string + nullable: true + other_uses_en: + type: string + nullable: true + native_range: + type: string + nullable: true + native_range_de: + type: string + nullable: true + native_range_en: + type: string + nullable: true + invasiveness: + type: string + nullable: true + pollination_type: + type: string + nullable: true + plant_layer: + type: string + nullable: true + nitrogen_fixer: + type: boolean + nullable: true + dynamic_accumulator: + type: boolean + nullable: true + dynamic_accumulator_nutrients: + type: array + items: + type: string + nullable: true + attracts_pollinators: + type: boolean + nullable: true + attracts_beneficial_insects: + type: boolean + nullable: true + wildlife_value: + type: string + nullable: true + wildlife_value_de: + type: string + nullable: true + wildlife_value_en: + type: string + nullable: true + mulch_plant: + type: boolean + nullable: true + ground_cover_quality: + type: string + nullable: true + allelopathic: + type: boolean + nullable: true + guild_role: + type: array + items: + type: string + nullable: true + succession_stage: + type: string + nullable: true + heavy_metal_tolerance: + type: boolean + nullable: true + wikidata_qid: + type: string + nullable: true + gbif_id: + type: string + nullable: true + eppo_code: + type: string + nullable: true + pfaf_url: + type: string + nullable: true + nectar_value: + type: integer + nullable: true + pollen_value: + type: integer + nullable: true + wild_bee_count: + type: integer + nullable: true + wild_bee_specialist_count: + type: integer + nullable: true + butterfly_moth_count: + type: integer + nullable: true + caterpillar_host_count: + type: integer + nullable: true + caterpillar_specialist_count: + type: integer + nullable: true + hoverfly_count: + type: integer + nullable: true + beetle_count: + type: integer + nullable: true + bird_count: + type: integer + nullable: true + mammal_count: + type: integer + nullable: true + native_status: + type: string + nullable: true + naturadb_tags: + type: string + nullable: true + primary_image_key: + type: string + nullable: true + source_urls: + type: array + items: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CreateSpecies: + type: object + required: [family_id, name_scientific] + properties: + family_id: + type: string + format: uuid + name_scientific: + type: string + name_en: + type: string + name_de: + type: string + description: + type: string + description_de: + type: string + description_en: + type: string + plant_layer: + type: string + nitrogen_fixer: + type: boolean + dynamic_accumulator: + type: boolean + dynamic_accumulator_nutrients: + type: array + items: + type: string + drought_tolerance: + type: string + native_status: + type: string + nectar_value: + type: integer + pollen_value: + type: integer + ph_min: + type: number + ph_max: + type: number + soil_texture_preference: + type: array + items: + type: string + hardiness_zone_usda: + type: string + hardiness_zone_at: + type: string + min_temp: + type: number + max_temp: + type: number + salt_tolerance: + type: string + edibility_rating: + type: integer + food_uses: + type: string + food_uses_de: + type: string + food_uses_en: + type: string + medicinal_uses: + type: string + medicinal_uses_de: + type: string + medicinal_uses_en: + type: string + other_uses: + type: string + other_uses_de: + type: string + other_uses_en: + type: string + native_range: + type: string + native_range_de: + type: string + native_range_en: + type: string + invasiveness: + type: string + pollination_type: + type: string + attracts_pollinators: + type: boolean + attracts_beneficial_insects: + type: boolean + wildlife_value: + type: string + wildlife_value_de: + type: string + wildlife_value_en: + type: string + mulch_plant: + type: boolean + ground_cover_quality: + type: string + allelopathic: + type: boolean + guild_role: + type: array + items: + type: string + succession_stage: + type: string + heavy_metal_tolerance: + type: boolean + wikidata_qid: + type: string + gbif_id: + type: string + eppo_code: + type: string + pfaf_url: + type: string + wild_bee_count: + type: integer + wild_bee_specialist_count: + type: integer + butterfly_moth_count: + type: integer + caterpillar_host_count: + type: integer + caterpillar_specialist_count: + type: integer + hoverfly_count: + type: integer + beetle_count: + type: integer + bird_count: + type: integer + mammal_count: + type: integer + naturadb_tags: + type: string + source_urls: + type: array + items: + type: string + + PaginatedSpecies: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Species" + total: + type: integer + page: + type: integer + per_page: + type: integer + + # ── Cultivar ──────────────────────────────────────────────────── + Cultivar: + type: object + description: A named cultivar/variety with growing parameters and scheduling + properties: + id: + type: string + format: uuid + slug: + type: string + species_id: + type: string + format: uuid + name: + type: string + name_en: + type: string + nullable: true + name_de: + type: string + nullable: true + name_scientific: + type: string + nullable: true + description: + type: string + nullable: true + description_de: + type: string + nullable: true + description_en: + type: string + nullable: true + is_organic: + type: boolean + perennial: + type: boolean + growing_time_days: + type: integer + nullable: true + planting_depth_cm: + type: number + nullable: true + row_spacing_cm: + type: number + nullable: true + plant_spacing_cm: + type: number + nullable: true + days_to_germination: + type: integer + nullable: true + germination_temp_c: + type: number + nullable: true + light_requirement: + type: string + nullable: true + stratification_required: + type: boolean + nullable: true + stratification_days: + type: integer + nullable: true + scarification_required: + type: boolean + nullable: true + seed_viability_years: + type: integer + nullable: true + storage_temp_c: + type: number + nullable: true + storage_humidity: + type: string + nullable: true + storage_notes: + type: string + nullable: true + min_temp: + type: number + nullable: true + max_temp: + type: number + nullable: true + humidity: + type: string + nullable: true + light: + type: string + nullable: true + frost_tolerance: + type: string + nullable: true + min_light_hours_day: + type: number + nullable: true + optimal_light_hours_day: + type: number + nullable: true + greenhouse_min_temp_c: + type: number + nullable: true + indoor_season_extension_weeks: + type: integer + nullable: true + ventilation_requirement: + type: string + nullable: true + heating_required: + type: boolean + nullable: true + indoor_sowing_months: + type: array + items: + type: integer + nullable: true + description: Month numbers (1-12) + direct_sowing_months: + type: array + items: + type: integer + nullable: true + transplanting_months: + type: array + items: + type: integer + nullable: true + glasshouse_months: + type: array + items: + type: integer + nullable: true + harvesting_months: + type: array + items: + type: integer + nullable: true + indoor_sowing_weeks: + type: array + items: + type: integer + nullable: true + description: ISO week numbers (1-52) + direct_sowing_weeks: + type: array + items: + type: integer + nullable: true + transplanting_weeks: + type: array + items: + type: integer + nullable: true + glasshouse_weeks: + type: array + items: + type: integer + nullable: true + harvesting_weeks: + type: array + items: + type: integer + nullable: true + succession_planting_days: + type: integer + nullable: true + planting_notes: + type: string + nullable: true + propagation_methods: + type: array + items: + type: string + nullable: true + cutting_season: + type: string + nullable: true + rootstock_species_id: + type: string + format: uuid + nullable: true + years_to_first_harvest: + type: integer + nullable: true + productive_lifespan_years: + type: integer + nullable: true + expected_yield_kg_per_m2: + type: number + nullable: true + yield_unit: + type: string + nullable: true + expected_yield_value: + type: number + nullable: true + harvest_window_days: + type: integer + nullable: true + storage_method: + type: array + items: + type: string + nullable: true + shelf_life_days: + type: integer + nullable: true + cold_storage_days: + type: integer + nullable: true + pollination_group: + type: string + nullable: true + self_fertile: + type: boolean + nullable: true + rootstock_compatibility: + type: string + nullable: true + wikidata_qid: + type: string + nullable: true + gbif_id: + type: string + nullable: true + pfaf_url: + type: string + nullable: true + primary_image_key: + type: string + nullable: true + source_urls: + type: array + items: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CreateCultivar: + type: object + required: [species_id, name] + properties: + species_id: + type: string + format: uuid + name: + type: string + name_en: + type: string + name_de: + type: string + name_scientific: + type: string + description: + type: string + description_de: + type: string + description_en: + type: string + is_organic: + type: boolean + perennial: + type: boolean + growing_time_days: + type: integer + planting_depth_cm: + type: number + row_spacing_cm: + type: number + plant_spacing_cm: + type: number + days_to_germination: + type: integer + germination_temp_c: + type: number + light_requirement: + type: string + indoor_sowing_months: + type: array + items: + type: integer + direct_sowing_months: + type: array + items: + type: integer + transplanting_months: + type: array + items: + type: integer + glasshouse_months: + type: array + items: + type: integer + harvesting_months: + type: array + items: + type: integer + indoor_sowing_weeks: + type: array + items: + type: integer + direct_sowing_weeks: + type: array + items: + type: integer + transplanting_weeks: + type: array + items: + type: integer + glasshouse_weeks: + type: array + items: + type: integer + harvesting_weeks: + type: array + items: + type: integer + source_urls: + type: array + items: + type: string + + PaginatedCultivars: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Cultivar" + total: + type: integer + page: + type: integer + per_page: + type: integer + + # ── Supplier ──────────────────────────────────────────────────── + Supplier: + type: object + properties: + id: + type: string + format: uuid + slug: + type: string + name: + type: string + url: + type: string + nullable: true + is_organic: + type: boolean + is_demeter: + type: boolean + country: + type: string + nullable: true + notes: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CreateSupplier: + type: object + required: [name] + properties: + name: + type: string + url: + type: string + is_organic: + type: boolean + is_demeter: + type: boolean + country: + type: string + notes: + type: string + + CultivarSupplier: + type: object + properties: + id: + type: string + format: uuid + cultivar_id: + type: string + format: uuid + supplier_id: + type: string + format: uuid + article_number: + type: string + nullable: true + product_url: + type: string + nullable: true + price_eur: + type: number + nullable: true + pack_size: + type: number + nullable: true + pack_unit: + type: string + nullable: true + last_checked_at: + type: string + format: date-time + nullable: true + created_at: + type: string + format: date-time + + CreateCultivarSupplier: + type: object + required: [supplier_id] + properties: + supplier_id: + type: string + format: uuid + article_number: + type: string + product_url: + type: string + price_eur: + type: number + pack_size: + type: number + pack_unit: + type: string + + # ── Companions ────────────────────────────────────────────────── + CompanionRelationship: + type: object + properties: + id: + type: string + format: uuid + species_a_id: + type: string + format: uuid + species_b_id: + type: string + format: uuid + relationship: + type: string + description: "e.g. beneficial, antagonistic, neutral" + mechanism: + type: string + nullable: true + source_url: + type: string + nullable: true + created_at: + type: string + format: date-time + + CompanionWithNames: + type: object + description: Companion relationship with resolved species names and images + properties: + id: + type: string + format: uuid + species_a_id: + type: string + format: uuid + species_b_id: + type: string + format: uuid + relationship: + type: string + mechanism: + type: string + nullable: true + source_url: + type: string + nullable: true + created_at: + type: string + format: date-time + species_a_scientific: + type: string + species_a_de: + type: string + nullable: true + species_a_slug: + type: string + species_a_image: + type: string + nullable: true + species_b_scientific: + type: string + species_b_de: + type: string + nullable: true + species_b_slug: + type: string + species_b_image: + type: string + nullable: true + + CreateCompanion: + type: object + required: [species_a_id, species_b_id, relationship] + properties: + species_a_id: + type: string + format: uuid + species_b_id: + type: string + format: uuid + relationship: + type: string + mechanism: + type: string + source_url: + type: string + + # ── Images ────────────────────────────────────────────────────── + Image: + type: object + properties: + id: + type: string + format: uuid + entity_type: + type: string + entity_id: + type: string + format: uuid + s3_key: + type: string + caption: + type: string + nullable: true + source_url: + type: string + nullable: true + license: + type: string + nullable: true + is_primary: + type: boolean + created_at: + type: string + format: date-time + + # ── Search ────────────────────────────────────────────────────── + SearchResult: + type: object + properties: + entity_type: + type: string + description: "family, species, or cultivar" + id: + type: string + format: uuid + slug: + type: string + name: + type: string + description: + type: string + nullable: true + rank: + type: number + description: Full-text search relevance score + + # ── Auth ──────────────────────────────────────────────────────── + MeResponse: + type: object + properties: + id: + type: string + format: uuid + email: + type: string + name: + type: string + nullable: true + nickname: + type: string + nullable: true + admin: + type: boolean diff --git a/herbapi-api/src/api/docs.rs b/herbapi-api/src/api/docs.rs new file mode 100644 index 0000000..4d9888b --- /dev/null +++ b/herbapi-api/src/api/docs.rs @@ -0,0 +1,43 @@ +use axum::http::{header, HeaderValue}; +use axum::response::{Html, IntoResponse}; + +const OPENAPI_SPEC: &str = include_str!("../../openapi.yaml"); + +pub async fn openapi_yaml() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, HeaderValue::from_static("text/yaml; charset=utf-8"))], + OPENAPI_SPEC, + ) +} + +pub async fn swagger_ui() -> impl IntoResponse { + Html(r#" + + + + HerbAPI — API Documentation + + + + +
+ + + +"#) +}