From 5f64b4783e4a961bfd04cdea6a16742d261f9aa4 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Wed, 18 Dec 2024 13:01:43 +0200 Subject: [PATCH 01/13] wip --- webapp/dev/user.clj | 95 ++++++++++- webapp/src/cljc/lipas/data/activities.cljc | 14 +- webapp/src/cljc/lipas/data/prop_types.cljc | 15 +- webapp/src/cljc/lipas/data/sports_sites.cljc | 157 ++++++++++++++++++- webapp/src/cljc/lipas/data/types_old.cljc | 3 + 5 files changed, 272 insertions(+), 12 deletions(-) diff --git a/webapp/dev/user.clj b/webapp/dev/user.clj index ef02284db..f46e35baf 100644 --- a/webapp/dev/user.clj +++ b/webapp/dev/user.clj @@ -64,6 +64,7 @@ (reindex-search!) (reindex-analytics!) (reset-admin-password! "kissa13") + (reset-password! "valtteri.harmainen@gmail.com" "kissa13") (require '[migratus.core :as migratus]) (migratus/create nil "activities_status" :sql) @@ -134,7 +135,7 @@ "Legacy lipas supports only these. Others will be treated as strings" {"boolean" "boolean" "numeric" "numberic" - "string" "string"}) + "string" "string"}) (doseq [[legacy-prop-k prop-k] legacy-mapping] (println @@ -151,7 +152,7 @@ (get-in prop-types/all [prop-k :description :en]) (name legacy-prop-k)))) - (doseq [p new-props + (doseq [p new-props [type-code m] types/all] (when (contains? (set (keys (:props m))) p) (println (str "-- Type " type-code)) @@ -167,4 +168,94 @@ (name (legacy-mapping-reverse p))) (get-in types/all [type-code :props p :priority]))))) + (require '[malli.provider :as mp]) + (require '[lipas.data.types :as types]) + (require '[lipas.backend.core :as core]) + + (def all-sites (atom {})) + + (doseq [type-code (keys types/all)] + (let [sites (core/get-sports-sites-by-type-code (db) type-code)] + (swap! all-sites (fn [m] + (reduce (fn [res site] (assoc res (:lipas-id site) site)) + m + sites))))) + + (mp/provide (vals @all-sites)) + (def schema *1) + + (require '[lipas.data.sports-sites :as ss]) + (require '[malli.core :as m]) + + (m/schema ss/base-schema) + + (m/validate ss/base-schema (-> @all-sites first second)) + + + (m/schema [:string {:min 1 :max 200}]) + (m/schema [:string {:min 1 :max 2048}]) + + + (require '[lipas.data.prop-types :as prop-types]) + + (m/schema [:set (into [:enum] (keys (:opts (:surface-material prop-types/all))))]) + + (require '[malli.dev]) + (malli.dev/start!) + + (require '[malli.error :as me]) + + (def results + (for [[lipas-id site] @all-sites] + [lipas-id (me/humanize (m/explain ss/base-schema site))])) + + (->> results + (filter #(some? (second %)))) + + (:activities (@all-sites 613971)) + + (count *1) + (@all-sites 613811) + + (require '[malli.json-schema :as mj]) + (mj/transform ss/base-schema) + + + (require '[lipas.data.types-new :as types-new]) + (require '[lipas.data.types-old :as types-old]) + + (require '[clojure.set :as set]) + + (def new-codes (set/difference + (set (keys types-new/all)) + (set (keys types-old/all)))) + + (-> types-new/all + (select-keys new-codes) + (->> (map (juxt first (comp :fi :name second))) + (sort-by first))) + + (def merged-codes (set/difference + (set (keys types-old/active)) + (set (keys types-new/active)))) + + (-> types-old/all + (select-keys merged-codes) + (->> (map (juxt first (comp :fi :name second))) + (sort-by first))) + + (def merged-to [101 103 106 203 4320 4510 4510]) + (-> types-new/all + (select-keys merged-to) + (->> (map (juxt first (comp :fi :name second))) + (sort-by first))) + + (require '[lipas.data.prop-types-new :as prop-types-new]) + (require '[lipas.data.prop-types-old :as prop-types-old]) + + (def new-prop-types (set/difference + (set (keys prop-types-new/all)) + (set (keys prop-types-old/all)))) + + ) diff --git a/webapp/src/cljc/lipas/data/activities.cljc b/webapp/src/cljc/lipas/data/activities.cljc index f05dcef42..1246f8d3b 100644 --- a/webapp/src/cljc/lipas/data/activities.cljc +++ b/webapp/src/cljc/lipas/data/activities.cljc @@ -261,10 +261,10 @@ {:schema [:sequential [:map [:organization {:optional true} localized-string-schema] - [:role {:optional true} (into [:enum] (keys contact-roles))] - [:email {:optional true} [:string]] - [:www {:optional true} [:string]] - [:phone-number {:optional true} [:string]]]] + [:role {:optional true} [:sequential (into [:enum] (keys contact-roles))]] + [:email {:optional true} localized-string-schema] + [:www {:optional true} localized-string-schema] + [:phone-number {:optional true} localized-string-schema]]] :field {:type "contacts" :description {:fi "Syötä kohteesta vastaavien tahojen yhteystiedot" @@ -401,7 +401,7 @@ :en "Copyright information"}}}}}} :additional-info-link - {:schema [:string] + {:schema localized-string-schema :field {:type "text-field" :description {:fi "Linkki ulkoisella sivustolla sijaitsevaan laajempaan kohde-esittelyyn" @@ -647,14 +647,14 @@ [:developmentally-disabled {:optional true} localized-string-schema]]] [:route-name {:optional true} localized-string-schema] [:outdoor-recreation-activities {:optional true} - [:sequential [:enum (keys outdoor-recreation-routes-activities)]]] + [:sequential (into [:enum] (keys outdoor-recreation-routes-activities))]] [:duration {:optional true} duration-schema] [:travel-direction {:optional true} [:enum "clockwise" "counter-clockwise"]] [:route-marking {:optional true} localized-string-schema] [:rules-structured {:optional true} rules-structured-schema] [:route-length-km {:optional true} number-schema] [:surface-material {:optional true} surface-material-schema] - [:accessibility-classification + [:accessibility-classification {:optional true} (into [:enum] (keys accessibility-classification))] [:independent-entity {:optional true} [:boolean]] pilgrimage-key-schema])] diff --git a/webapp/src/cljc/lipas/data/prop_types.cljc b/webapp/src/cljc/lipas/data/prop_types.cljc index 35a80236e..e6219cde2 100644 --- a/webapp/src/cljc/lipas/data/prop_types.cljc +++ b/webapp/src/cljc/lipas/data/prop_types.cljc @@ -2,7 +2,8 @@ "Type codes went through a major overhaul in the summer of 2024. This namespace represents the changes made." (:require - [lipas.data.types :as types])) + [lipas.data.types :as types] + [lipas.data.materials :as materials])) (def all {:height-m @@ -34,6 +35,7 @@ {:name {:fi "Pintamateriaali", :se "Ytmaterial", :en "Surface material"}, :data-type "enum-coll", + :opts materials/surface-materials :description {:fi "Liikunta-alueiden pääasiallinen pintamateriaali - tarkempi kuvaus liikuntapaikan eri tilojen pintamateriaalista voidaan antaa pintamateriaalin lisätietokentässä", @@ -817,7 +819,7 @@ :se "Ange antalet delar som utrymmet kan delas in i", :en "Enter the number of sections into which the space can be divided"}, - :data-type "number", + :data-type "numeric", :description {:fi "Onko tila jaettavissa osiin esim. jakoseinien tai -verhojen avulla", @@ -1818,3 +1820,12 @@ (def used (let [used (set (mapcat (comp keys :props second) types/all))] (select-keys all used))) + +(def schemas + (into {} (for [[k m] all] + [k (case (:data-type m) + "string" [:string] + "numeric" [:or [:int] [:double]] + "boolean" [:boolean] + "enum" (into [:enum] (keys (:opts m))) + "enum-coll" [:sequential (into [:enum] (keys (:opts m)))])]))) diff --git a/webapp/src/cljc/lipas/data/sports_sites.cljc b/webapp/src/cljc/lipas/data/sports_sites.cljc index fb966aab7..498f5c6c7 100644 --- a/webapp/src/cljc/lipas/data/sports_sites.cljc +++ b/webapp/src/cljc/lipas/data/sports_sites.cljc @@ -1,5 +1,15 @@ (ns lipas.data.sports-sites - (:require [lipas.data.status :as status])) + (:require [lipas.data.activities :as activities] + [lipas.data.admins :as admins] + [lipas.data.cities :as cities] + [lipas.data.owners :as owners] + [lipas.data.prop-types :as prop-types] + [lipas.data.status :as status] + [lipas.data.types :as types] + [lipas.schema.core :as specs] + [lipas.utils :as utils] + [malli.core :as m] + [malli.util :as mu])) (def document-statuses {"draft" @@ -18,3 +28,148 @@ {:fi "Salibandykenttä" :en "Floorball field" :se "Innebandyplan"}}) + +(def circumstances-schema + [:map + [:storage-capacity {:optional true} :string] + [:roof-trusses-operation-model {:optional true} :string] + [:general-information {:optional true} :string] + [:corner-pieces-count {:optional true} :int] + [:audience-toilets-count {:optional true} :int] + [:bus-park-capacity {:optional true} :int] + [:wifi-capacity-sufficient-for-streaming? {:optional true} :boolean] + [:first-aid-comment {:optional true} :string] + [:fixed-cameras? {:optional true} :boolean] + [:stretcher? {:optional true} :boolean] + [:field-level-loading-doors? {:optional true} :boolean] + [:car-parking-economics-model {:optional true} :string] + [:vip-area? {:optional true} :boolean] + [:separate-referee-locker-room? {:optional true} :boolean] + [:audit-date {:optional true} :string] + [:led-screens-or-surfaces-for-ads? {:optional true} :boolean] + [:saunas-count {:optional true} :int] + [:camera-stands? {:optional true} :boolean] + [:wired-microfone-quantity {:optional true} :int] + [:locker-rooms-count {:optional true} :int] + [:car-parking-capacity {:optional true} :int] + [:broadcast-car-park? {:optional true} :boolean] + [:press-conference-space? {:optional true} :boolean] + [:open-floor-space-length-m {:optional true} number?] + [:wifi-available? {:optional true} :boolean] + [:goal-shrinking-elements-count {:optional true} :int] + [:scoreboard-count {:optional true} :int] + [:restaurateur-contact-info {:optional true} :string] + [:vip-area-comment {:optional true} :string] + [:cafe-or-restaurant-has-exclusive-rights-for-products? + {:optional true} + :boolean] + [:gym? {:optional true} :boolean] + [:ticket-sales-operator {:optional true} :string] + [:side-training-space? {:optional true} :boolean] + [:locker-room-quality-comment {:optional true} :string] + [:roof-trusses? {:optional true} :boolean] + [:detached-chair-quantity {:optional true} :int] + [:conference-space-total-capacity-person {:optional true} :int] + [:iff-certification-stickers-in-goals? {:optional true} :boolean] + [:electrical-plan-available? {:optional true} :boolean] + [:audio-mixer-available? {:optional true} :boolean] + [:iff-certified-rink? {:optional true} :boolean] + [:teams-using {:optional true} :string] + [:loading-equipment-available? {:optional true} :boolean] + [:doping-test-facilities? {:optional true} :boolean] + [:wireless-microfone-quantity {:optional true} :int] + [:defibrillator? {:optional true} :boolean] + [:open-floor-space-width-m {:optional true} number?] + [:cafeteria-and-restaurant-capacity-person {:optional true} :int] + [:speakers-aligned-towards-stands? {:optional true} :boolean] + [:conference-space-quantity {:optional true} :int] + [:three-phase-electric-power? {:optional true} :boolean] + [:roof-trusses-capacity-kg {:optional true} :int] + [:open-floor-space-area-m2 {:optional true} number?] + [:detached-tables-quantity {:optional true} :int] + [:available-goals-count {:optional true} :int] + [:player-entrance {:optional true} :string]]) + +(def number-schema [:or :double :int]) + +(def geom-type->schema + {"Point" [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "Point"]] + [:coordinates [:vector {:min 2 :max 3} number-schema]]]]] + "LineString" [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "LineString"]] + [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] + [:properties {:optional true} + [:map + [:name {:optional true} :string] + [:lipas-id {:optional true} :int] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]]]]] + "Polygon" [:map + [:type [:enum "Feature"]] + [:geometry + [:map + [:type [:enum "Polygon"]] + [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]}) + +(def base-schema + ;; TODO audit + [:map + [:lipas-id [:int]] + [:status (into [:enum] (keys statuses))] + [:name [:string {:min 2 :max 100}]] + [:marketing-name {:optional true} + [:string {:min 2 :max 100}]] + [:name-localized {:optional true} + [:map + [:se {:optional true} + [:string {:min 2 :max 100}]] + [:en {:optional true} + [:string {:min 2 :max 100}]]]] + [:owner (into [:enum] (keys owners/all))] + [:admin (into [:enum] (keys admins/all))] + [:email {:optional true} + [:re specs/email-regex]] + [:www {:optional true} + [:string {:min 1 :max 500}]] + [:reservations-link {:optional true} + [:string {:min 1 :max 500}]] + [:phone-number {:optional true} + [:string {:min 1 :max 50}]] + [:comment {:optional true} + [:string {:min 1 :max 2048}]] + [:construction-year {:optional true} + [:int {:min 1800 :max (+ 10 utils/this-year)}]] + [:renovation-years {:optional true} + [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]] + [:location + [:map + [:city + [:map + [:city-code (into [:enum] (keys cities/by-city-code))] + [:neighborhood {:optional true} + [:string {:min 1 :max 100}]]]] + [:address [:string {:min 1 :max 200}]] + [:postal-code [:re specs/postal-code-regex]] + [:postal-office {:optional true} + [:string {:min 1 :max 100}]] + [:geometries + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:vector + (into [:or] (vals geom-type->schema))]]]]]] + [:circumstances {:optional true} circumstances-schema] + [:activities {:optional true} activities/activities-schema] + [:properties {:optional true} + (into [:map] (for [[k schema] prop-types/schemas] + [k {:optional true} schema]))]]) diff --git a/webapp/src/cljc/lipas/data/types_old.cljc b/webapp/src/cljc/lipas/data/types_old.cljc index b77159962..6e9922c8f 100644 --- a/webapp/src/cljc/lipas/data/types_old.cljc +++ b/webapp/src/cljc/lipas/data/types_old.cljc @@ -4391,3 +4391,6 @@ (def sub-category-by-fi-name (utils/index-by (comp :fi :name) (vals sub-categories))) + +(def active + (reduce-kv (fn [m k v] (if (not= "active" (:status v)) (dissoc m k) m)) all all)) From 4110d24bfae4ab5f13ed343521c5eea39bea2682 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 18 Dec 2024 18:05:15 +0200 Subject: [PATCH 02/13] Add reitit api v2 routes and nginx server block --- docker-compose.yml | 4 + nginx/proxy_local.conf | 26 +++ webapp/src/clj/lipas/backend/api/v2.clj | 39 +++++ webapp/src/clj/lipas/backend/handler.clj | 4 +- webapp/src/cljc/lipas/data/sports_sites.cljc | 156 +---------------- .../src/cljc/lipas/schema/sports_sites.cljc | 162 ++++++++++++++++++ 6 files changed, 235 insertions(+), 156 deletions(-) create mode 100644 webapp/src/clj/lipas/backend/api/v2.clj create mode 100644 webapp/src/cljc/lipas/schema/sports_sites.cljc diff --git a/docker-compose.yml b/docker-compose.yml index 353ba0b53..9d2dd4252 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,10 @@ services: proxy-local: extends: service: proxy-base + ports: + - '80:80' + - '443:443' + - '444:444' links: - mapproxy - logstash diff --git a/nginx/proxy_local.conf b/nginx/proxy_local.conf index e9e58f77e..4d79b34fd 100644 --- a/nginx/proxy_local.conf +++ b/nginx/proxy_local.conf @@ -106,5 +106,31 @@ server { location / { try_files $uri $uri/ /index.html =404; } +} + +server { + listen 444 ssl; + ssl_certificate /certs/server.crt; + ssl_certificate_key /certs/server.key; + + # Make search engines ignore anything returned from this proxy + add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; + + server_name lipas-dev.cc.jyu.fi; + + gzip on; + gzip_vary on; + gzip_http_version 1.0; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_types application/javascript application/json application/transit+json; + # FIXME: Now the path prefix has to be same as in the Reitit tree + location /api-v2/ { + proxy_pass http://172.17.0.1:8091/api-v2/; + proxy_connect_timeout 300; + proxy_send_timeout 300; + proxy_read_timeout 300; + send_timeout 300; + } } diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj new file mode 100644 index 000000000..7b598309d --- /dev/null +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -0,0 +1,39 @@ +(ns lipas.backend.api.v2 + (:require [reitit.coercion.malli] + [reitit.swagger :as swagger] + [reitit.swagger-ui :as swagger-ui] + [ring.util.http-response :as resp] + [lipas.schema.sports-sites :as sports-sites-schema])) + +(defn routes [{:keys [db search] :as ctx}] + (let [ui-handler (swagger-ui/create-swagger-ui-handler + {:url "/api-v2/swagger.json"})] + ["/api-v2" + {:swagger {:id :api-v2} + :coercion reitit.coercion.malli/coercion} + + ["/sports-sites" + {:get {:handler (fn [_] + (resp/ok)) + :responses {200 {:body [:vector sports-sites-schema/base-schema]}}}}] + ["/lois" + {:get {:handler (fn [_] + (resp/ok))}}] + + ["/swagger.json" + {:get + {:no-doc true + :swagger {:info {:title "Lipas-API v2"} + :securityDefinitions + {:token-auth + {:type "apiKey" + :in "header" + :name "Authorization"}}} + :handler (swagger/create-swagger-handler)}}] + + ["/swagger-ui" + {:get {:no-doc true + :handler ui-handler}}] + ["/swagger-ui/*" + {:get {:no-doc true + :handler ui-handler}}]])) diff --git a/webapp/src/clj/lipas/backend/handler.clj b/webapp/src/clj/lipas/backend/handler.clj index 52d606c41..2ab8f59df 100644 --- a/webapp/src/clj/lipas/backend/handler.clj +++ b/webapp/src/clj/lipas/backend/handler.clj @@ -1,6 +1,7 @@ (ns lipas.backend.handler (:require [clojure.java.io :as io] [clojure.spec.alpha :as s] + [lipas.backend.api.v2 :as v2] [lipas.backend.core :as core] [lipas.backend.jwt :as jwt] [lipas.backend.middleware :as mw] @@ -717,7 +718,8 @@ {:status 200 :body (core/search-lois-with-params search body-params)})}}] - (ptv-handler/routes ctx)]] + (ptv-handler/routes ctx)] + (v2/routes ctx)] {:data {:coercion reitit.coercion.spec/coercion diff --git a/webapp/src/cljc/lipas/data/sports_sites.cljc b/webapp/src/cljc/lipas/data/sports_sites.cljc index 498f5c6c7..3c2df7581 100644 --- a/webapp/src/cljc/lipas/data/sports_sites.cljc +++ b/webapp/src/cljc/lipas/data/sports_sites.cljc @@ -1,15 +1,5 @@ (ns lipas.data.sports-sites - (:require [lipas.data.activities :as activities] - [lipas.data.admins :as admins] - [lipas.data.cities :as cities] - [lipas.data.owners :as owners] - [lipas.data.prop-types :as prop-types] - [lipas.data.status :as status] - [lipas.data.types :as types] - [lipas.schema.core :as specs] - [lipas.utils :as utils] - [malli.core :as m] - [malli.util :as mu])) + (:require [lipas.data.status :as status])) (def document-statuses {"draft" @@ -29,147 +19,3 @@ :en "Floorball field" :se "Innebandyplan"}}) -(def circumstances-schema - [:map - [:storage-capacity {:optional true} :string] - [:roof-trusses-operation-model {:optional true} :string] - [:general-information {:optional true} :string] - [:corner-pieces-count {:optional true} :int] - [:audience-toilets-count {:optional true} :int] - [:bus-park-capacity {:optional true} :int] - [:wifi-capacity-sufficient-for-streaming? {:optional true} :boolean] - [:first-aid-comment {:optional true} :string] - [:fixed-cameras? {:optional true} :boolean] - [:stretcher? {:optional true} :boolean] - [:field-level-loading-doors? {:optional true} :boolean] - [:car-parking-economics-model {:optional true} :string] - [:vip-area? {:optional true} :boolean] - [:separate-referee-locker-room? {:optional true} :boolean] - [:audit-date {:optional true} :string] - [:led-screens-or-surfaces-for-ads? {:optional true} :boolean] - [:saunas-count {:optional true} :int] - [:camera-stands? {:optional true} :boolean] - [:wired-microfone-quantity {:optional true} :int] - [:locker-rooms-count {:optional true} :int] - [:car-parking-capacity {:optional true} :int] - [:broadcast-car-park? {:optional true} :boolean] - [:press-conference-space? {:optional true} :boolean] - [:open-floor-space-length-m {:optional true} number?] - [:wifi-available? {:optional true} :boolean] - [:goal-shrinking-elements-count {:optional true} :int] - [:scoreboard-count {:optional true} :int] - [:restaurateur-contact-info {:optional true} :string] - [:vip-area-comment {:optional true} :string] - [:cafe-or-restaurant-has-exclusive-rights-for-products? - {:optional true} - :boolean] - [:gym? {:optional true} :boolean] - [:ticket-sales-operator {:optional true} :string] - [:side-training-space? {:optional true} :boolean] - [:locker-room-quality-comment {:optional true} :string] - [:roof-trusses? {:optional true} :boolean] - [:detached-chair-quantity {:optional true} :int] - [:conference-space-total-capacity-person {:optional true} :int] - [:iff-certification-stickers-in-goals? {:optional true} :boolean] - [:electrical-plan-available? {:optional true} :boolean] - [:audio-mixer-available? {:optional true} :boolean] - [:iff-certified-rink? {:optional true} :boolean] - [:teams-using {:optional true} :string] - [:loading-equipment-available? {:optional true} :boolean] - [:doping-test-facilities? {:optional true} :boolean] - [:wireless-microfone-quantity {:optional true} :int] - [:defibrillator? {:optional true} :boolean] - [:open-floor-space-width-m {:optional true} number?] - [:cafeteria-and-restaurant-capacity-person {:optional true} :int] - [:speakers-aligned-towards-stands? {:optional true} :boolean] - [:conference-space-quantity {:optional true} :int] - [:three-phase-electric-power? {:optional true} :boolean] - [:roof-trusses-capacity-kg {:optional true} :int] - [:open-floor-space-area-m2 {:optional true} number?] - [:detached-tables-quantity {:optional true} :int] - [:available-goals-count {:optional true} :int] - [:player-entrance {:optional true} :string]]) - -(def number-schema [:or :double :int]) - -(def geom-type->schema - {"Point" [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "Point"]] - [:coordinates [:vector {:min 2 :max 3} number-schema]]]]] - "LineString" [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "LineString"]] - [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] - [:properties {:optional true} - [:map - [:name {:optional true} :string] - [:lipas-id {:optional true} :int] - [:type-code {:optional true} :int] - [:route-part-difficulty {:optional true} :string] - [:travel-direction {:optional true} :string]]]]]] - "Polygon" [:map - [:type [:enum "Feature"]] - [:geometry - [:map - [:type [:enum "Polygon"]] - [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]}) - -(def base-schema - ;; TODO audit - [:map - [:lipas-id [:int]] - [:status (into [:enum] (keys statuses))] - [:name [:string {:min 2 :max 100}]] - [:marketing-name {:optional true} - [:string {:min 2 :max 100}]] - [:name-localized {:optional true} - [:map - [:se {:optional true} - [:string {:min 2 :max 100}]] - [:en {:optional true} - [:string {:min 2 :max 100}]]]] - [:owner (into [:enum] (keys owners/all))] - [:admin (into [:enum] (keys admins/all))] - [:email {:optional true} - [:re specs/email-regex]] - [:www {:optional true} - [:string {:min 1 :max 500}]] - [:reservations-link {:optional true} - [:string {:min 1 :max 500}]] - [:phone-number {:optional true} - [:string {:min 1 :max 50}]] - [:comment {:optional true} - [:string {:min 1 :max 2048}]] - [:construction-year {:optional true} - [:int {:min 1800 :max (+ 10 utils/this-year)}]] - [:renovation-years {:optional true} - [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]] - [:location - [:map - [:city - [:map - [:city-code (into [:enum] (keys cities/by-city-code))] - [:neighborhood {:optional true} - [:string {:min 1 :max 100}]]]] - [:address [:string {:min 1 :max 200}]] - [:postal-code [:re specs/postal-code-regex]] - [:postal-office {:optional true} - [:string {:min 1 :max 100}]] - [:geometries - [:map - [:type [:enum "FeatureCollection"]] - [:features - [:vector - (into [:or] (vals geom-type->schema))]]]]]] - [:circumstances {:optional true} circumstances-schema] - [:activities {:optional true} activities/activities-schema] - [:properties {:optional true} - (into [:map] (for [[k schema] prop-types/schemas] - [k {:optional true} schema]))]]) diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc new file mode 100644 index 000000000..ef0ac2ca3 --- /dev/null +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -0,0 +1,162 @@ +(ns lipas.schema.sports-sites + (:require [lipas.data.activities :as activities] + [lipas.data.admins :as admins] + [lipas.data.cities :as cities] + [lipas.data.owners :as owners] + [lipas.data.prop-types :as prop-types] + [lipas.data.sports-sites :as sports-sites] + [lipas.schema.core :as specs] + [lipas.utils :as utils])) + +(def circumstances-schema + [:map + [:storage-capacity {:optional true} :string] + [:roof-trusses-operation-model {:optional true} :string] + [:general-information {:optional true} :string] + [:corner-pieces-count {:optional true} :int] + [:audience-toilets-count {:optional true} :int] + [:bus-park-capacity {:optional true} :int] + [:wifi-capacity-sufficient-for-streaming? {:optional true} :boolean] + [:first-aid-comment {:optional true} :string] + [:fixed-cameras? {:optional true} :boolean] + [:stretcher? {:optional true} :boolean] + [:field-level-loading-doors? {:optional true} :boolean] + [:car-parking-economics-model {:optional true} :string] + [:vip-area? {:optional true} :boolean] + [:separate-referee-locker-room? {:optional true} :boolean] + [:audit-date {:optional true} :string] + [:led-screens-or-surfaces-for-ads? {:optional true} :boolean] + [:saunas-count {:optional true} :int] + [:camera-stands? {:optional true} :boolean] + [:wired-microfone-quantity {:optional true} :int] + [:locker-rooms-count {:optional true} :int] + [:car-parking-capacity {:optional true} :int] + [:broadcast-car-park? {:optional true} :boolean] + [:press-conference-space? {:optional true} :boolean] + [:open-floor-space-length-m {:optional true} number?] + [:wifi-available? {:optional true} :boolean] + [:goal-shrinking-elements-count {:optional true} :int] + [:scoreboard-count {:optional true} :int] + [:restaurateur-contact-info {:optional true} :string] + [:vip-area-comment {:optional true} :string] + [:cafe-or-restaurant-has-exclusive-rights-for-products? + {:optional true} + :boolean] + [:gym? {:optional true} :boolean] + [:ticket-sales-operator {:optional true} :string] + [:side-training-space? {:optional true} :boolean] + [:locker-room-quality-comment {:optional true} :string] + [:roof-trusses? {:optional true} :boolean] + [:detached-chair-quantity {:optional true} :int] + [:conference-space-total-capacity-person {:optional true} :int] + [:iff-certification-stickers-in-goals? {:optional true} :boolean] + [:electrical-plan-available? {:optional true} :boolean] + [:audio-mixer-available? {:optional true} :boolean] + [:iff-certified-rink? {:optional true} :boolean] + [:teams-using {:optional true} :string] + [:loading-equipment-available? {:optional true} :boolean] + [:doping-test-facilities? {:optional true} :boolean] + [:wireless-microfone-quantity {:optional true} :int] + [:defibrillator? {:optional true} :boolean] + [:open-floor-space-width-m {:optional true} number?] + [:cafeteria-and-restaurant-capacity-person {:optional true} :int] + [:speakers-aligned-towards-stands? {:optional true} :boolean] + [:conference-space-quantity {:optional true} :int] + [:three-phase-electric-power? {:optional true} :boolean] + [:roof-trusses-capacity-kg {:optional true} :int] + [:open-floor-space-area-m2 {:optional true} number?] + [:detached-tables-quantity {:optional true} :int] + [:available-goals-count {:optional true} :int] + [:player-entrance {:optional true} :string]]) + +;; FIXME: :or doesn't work nicely with OpenAPI +(def number-schema [:or :double :int]) + +(def geom-type->schema + {"Point" [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "Point"]] + [:coordinates [:vector {:min 2 :max 3} number-schema]]]]] + "LineString" [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "LineString"]] + [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] + [:properties {:optional true} + [:map + [:name {:optional true} :string] + [:lipas-id {:optional true} :int] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]]]]] + "Polygon" [:map + [:type [:enum "Feature"]] + [:geometry + [:map + [:type [:enum "Polygon"]] + [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]}) + +(def base-schema + ;; TODO audit + [:map + {:title "Sports site" + :description ""} + [:lipas-id [:int]] + [:status (into [:enum] (keys sports-sites/statuses))] + [:name [:string {:min 2 :max 100}]] + [:marketing-name {:optional true} + [:string {:min 2 :max 100}]] + [:name-localized {:optional true} + [:map + [:se {:optional true} + [:string {:min 2 :max 100}]] + [:en {:optional true} + [:string {:min 2 :max 100}]]]] + [:owner (into [:enum] (keys owners/all))] + [:admin (into [:enum] (keys admins/all))] + [:email {:optional true} + [:re specs/email-regex]] + [:www {:optional true} + [:string {:min 1 :max 500}]] + [:reservations-link {:optional true} + [:string {:min 1 :max 500}]] + [:phone-number {:optional true} + [:string {:min 1 :max 50}]] + [:comment {:optional true} + [:string {:min 1 :max 2048}]] + [:construction-year {:optional true} + [:int {:min 1800 :max (+ 10 utils/this-year)}]] + [:renovation-years {:optional true} + [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]] + [:location + [:map + [:city + [:map + [:city-code (into [:enum] (keys cities/by-city-code))] + [:neighborhood {:optional true} + [:string {:min 1 :max 100}]]]] + [:address [:string {:min 1 :max 200}]] + [:postal-code [:re specs/postal-code-regex]] + [:postal-office {:optional true} + [:string {:min 1 :max 100}]] + [:geometries + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:vector + ;; FIXME: :or only the first entry is included in OpenAPI + ;; Should be an multi schema? (do those work nice with OpenAPI) + (into [:or] (vals geom-type->schema))]]]]]] + [:circumstances + {:optional true + :description "Floorball information"} + circumstances-schema] + [:activities {:optional true} activities/activities-schema] + [:properties {:optional true} + (into [:map] (for [[k schema] prop-types/schemas] + [k {:optional true} schema]))]]) From 570b47d2eda77431ac0342ec32008c0aa4f3952b Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 19 Dec 2024 11:04:25 +0200 Subject: [PATCH 03/13] Use OpenAPI v3 instead, remove :or schemas --- webapp/src/clj/lipas/backend/api/v2.clj | 17 ++-- .../src/cljc/lipas/schema/sports_sites.cljc | 79 +++++++++++-------- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 7b598309d..86a00c016 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -1,15 +1,15 @@ (ns lipas.backend.api.v2 (:require [reitit.coercion.malli] - [reitit.swagger :as swagger] + [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] [ring.util.http-response :as resp] [lipas.schema.sports-sites :as sports-sites-schema])) (defn routes [{:keys [db search] :as ctx}] (let [ui-handler (swagger-ui/create-swagger-ui-handler - {:url "/api-v2/swagger.json"})] + {:url "/api-v2/openapi.json"})] ["/api-v2" - {:swagger {:id :api-v2} + {:openapi {:id :api-v2} :coercion reitit.coercion.malli/coercion} ["/sports-sites" @@ -20,16 +20,11 @@ {:get {:handler (fn [_] (resp/ok))}}] - ["/swagger.json" + ["/openapi.json" {:get {:no-doc true - :swagger {:info {:title "Lipas-API v2"} - :securityDefinitions - {:token-auth - {:type "apiKey" - :in "header" - :name "Authorization"}}} - :handler (swagger/create-swagger-handler)}}] + :swagger {:info {:title "Lipas-API v2"}} + :handler (openapi/create-openapi-handler)}}] ["/swagger-ui" {:get {:no-doc true diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index ef0ac2ca3..9eb94b359 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -6,7 +6,8 @@ [lipas.data.prop-types :as prop-types] [lipas.data.sports-sites :as sports-sites] [lipas.schema.core :as specs] - [lipas.utils :as utils])) + [lipas.utils :as utils] + [malli.json-schema :as json-schema])) (def circumstances-schema [:map @@ -69,37 +70,46 @@ [:available-goals-count {:optional true} :int] [:player-entrance {:optional true} :string]]) -;; FIXME: :or doesn't work nicely with OpenAPI -(def number-schema [:or :double :int]) +;; https://github.com/metosin/malli/issues/670 +(def number-schema number?) -(def geom-type->schema - {"Point" [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "Point"]] - [:coordinates [:vector {:min 2 :max 3} number-schema]]]]] - "LineString" [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "LineString"]] - [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] - [:properties {:optional true} - [:map - [:name {:optional true} :string] - [:lipas-id {:optional true} :int] - [:type-code {:optional true} :int] - [:route-part-difficulty {:optional true} :string] - [:travel-direction {:optional true} :string]]]]]] - "Polygon" [:map - [:type [:enum "Feature"]] - [:geometry - [:map - [:type [:enum "Polygon"]] - [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]}) +;; FIXME: multi schema -> json-schema :oneOf, swagger-ui still displays just the +;; first alternative. +(def geometries-feature + [:multi {:dispatch (fn [x] + (-> x :geometry :type))} + ["Point" + [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "Point"]] + [:coordinates [:vector {:min 2 :max 3} number-schema]]]]]] + + ["LineString" + [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry + [:map + [:type [:enum "LineString"]] + [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] + [:properties {:optional true} + [:map + [:name {:optional true} :string] + [:lipas-id {:optional true} :int] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]]]]]] + + ["Polygon" + [:map + [:type [:enum "Feature"]] + [:geometry + [:map + [:type [:enum "Polygon"]] + [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]] ]) (def base-schema ;; TODO audit @@ -149,9 +159,7 @@ [:type [:enum "FeatureCollection"]] [:features [:vector - ;; FIXME: :or only the first entry is included in OpenAPI - ;; Should be an multi schema? (do those work nice with OpenAPI) - (into [:or] (vals geom-type->schema))]]]]]] + geometries-feature]]]]]] [:circumstances {:optional true :description "Floorball information"} @@ -160,3 +168,6 @@ [:properties {:optional true} (into [:map] (for [[k schema] prop-types/schemas] [k {:optional true} schema]))]]) + +(comment + (json-schema/transform geometries-feature)) From 94b2d58df69638eb6863b603a1d56ce1a00045b1 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 19 Dec 2024 14:09:01 +0200 Subject: [PATCH 04/13] Make each type-code a separate schema alternative --- webapp/project.clj | 1 + webapp/src/clj/lipas/backend/api/v2.clj | 10 +- webapp/src/cljc/lipas/data/prop_types.cljc | 2 +- .../src/cljc/lipas/schema/sports_sites.cljc | 181 +++++++++++------- 4 files changed, 123 insertions(+), 71 deletions(-) diff --git a/webapp/project.clj b/webapp/project.clj index 521d74617..62ea137ef 100644 --- a/webapp/project.clj +++ b/webapp/project.clj @@ -7,6 +7,7 @@ [com.taoensso/timbre "4.10.0"] [com.cemerick/url "0.1.1"] [metosin/reitit "0.7.1"] + [metosin/ring-swagger-ui "5.18.2"] [metosin/spec-tools "0.10.7"] [metosin/malli "0.16.2"] diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 86a00c016..4bb16e42f 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -1,9 +1,9 @@ (ns lipas.backend.api.v2 - (:require [reitit.coercion.malli] + (:require [lipas.schema.sports-sites :as sports-sites-schema] + [reitit.coercion.malli] [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] - [ring.util.http-response :as resp] - [lipas.schema.sports-sites :as sports-sites-schema])) + [ring.util.http-response :as resp])) (defn routes [{:keys [db search] :as ctx}] (let [ui-handler (swagger-ui/create-swagger-ui-handler @@ -15,7 +15,9 @@ ["/sports-sites" {:get {:handler (fn [_] (resp/ok)) - :responses {200 {:body [:vector sports-sites-schema/base-schema]}}}}] + :responses {200 {:body [:vector + {:title "SportSites"} + sports-sites-schema/sports-site]}}}}] ["/lois" {:get {:handler (fn [_] (resp/ok))}}] diff --git a/webapp/src/cljc/lipas/data/prop_types.cljc b/webapp/src/cljc/lipas/data/prop_types.cljc index e6219cde2..ecbb89004 100644 --- a/webapp/src/cljc/lipas/data/prop_types.cljc +++ b/webapp/src/cljc/lipas/data/prop_types.cljc @@ -1825,7 +1825,7 @@ (into {} (for [[k m] all] [k (case (:data-type m) "string" [:string] - "numeric" [:or [:int] [:double]] + "numeric" number? "boolean" [:boolean] "enum" (into [:enum] (keys (:opts m))) "enum-coll" [:sequential (into [:enum] (keys (:opts m)))])]))) diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index 9eb94b359..454830fa1 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -5,9 +5,11 @@ [lipas.data.owners :as owners] [lipas.data.prop-types :as prop-types] [lipas.data.sports-sites :as sports-sites] + [lipas.data.types :as types] [lipas.schema.core :as specs] [lipas.utils :as utils] - [malli.json-schema :as json-schema])) + [malli.json-schema :as json-schema] + [malli.util :as mu])) (def circumstances-schema [:map @@ -73,49 +75,60 @@ ;; https://github.com/metosin/malli/issues/670 (def number-schema number?) -;; FIXME: multi schema -> json-schema :oneOf, swagger-ui still displays just the -;; first alternative. -(def geometries-feature - [:multi {:dispatch (fn [x] - (-> x :geometry :type))} - ["Point" +(defn make-location-schema [geometry-schema] + [:map + [:city [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "Point"]] - [:coordinates [:vector {:min 2 :max 3} number-schema]]]]]] - - ["LineString" + [:city-code (into [:enum] (keys cities/by-city-code))] + [:neighborhood {:optional true} + [:string {:min 1 :max 100}]]]] + [:address [:string {:min 1 :max 200}]] + [:postal-code [:re specs/postal-code-regex]] + [:postal-office {:optional true} + [:string {:min 1 :max 100}]] + [:geometries [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry - [:map - [:type [:enum "LineString"]] - [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] - [:properties {:optional true} - [:map - [:name {:optional true} :string] - [:lipas-id {:optional true} :int] - [:type-code {:optional true} :int] - [:route-part-difficulty {:optional true} :string] - [:travel-direction {:optional true} :string]]]]]]] + [:type [:enum "FeatureCollection"]] + [:features + [:vector + [:map + [:type [:enum "Feature"]] + [:id {:optional true} :string] + [:geometry geometry-schema]]]]]]]) - ["Polygon" +(def point-geometry + [:map + {:title "Point"} + [:type [:enum "Point"]] + [:coordinates [:vector {:min 2 :max 3} number-schema]]]) + +(def line-string-geometry + [:map + {:title "LineString"} + [:type [:enum "LineString"]] + [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] + [:properties {:optional true} [:map - [:type [:enum "Feature"]] - [:geometry - [:map - [:type [:enum "Polygon"]] - [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]]]] ]) + [:name {:optional true} :string] + [:lipas-id {:optional true} :int] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]]]) -(def base-schema - ;; TODO audit + +(def polygon-geometry [:map - {:title "Sports site" - :description ""} + {:title "Polygon"} + [:type [:enum "Polygon"]] + [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]) + +(def point-location (make-location-schema point-geometry)) +(def line-string-location (make-location-schema line-string-geometry)) +(def polygon-location (make-location-schema polygon-geometry)) + +(def sports-site-base + [:map + {:title "Shared Properties"} [:lipas-id [:int]] [:status (into [:enum] (keys sports-sites/statuses))] [:name [:string {:min 2 :max 100}]] @@ -142,32 +155,68 @@ [:construction-year {:optional true} [:int {:min 1800 :max (+ 10 utils/this-year)}]] [:renovation-years {:optional true} - [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]] - [:location - [:map - [:city - [:map - [:city-code (into [:enum] (keys cities/by-city-code))] - [:neighborhood {:optional true} - [:string {:min 1 :max 100}]]]] - [:address [:string {:min 1 :max 200}]] - [:postal-code [:re specs/postal-code-regex]] - [:postal-office {:optional true} - [:string {:min 1 :max 100}]] - [:geometries - [:map - [:type [:enum "FeatureCollection"]] - [:features - [:vector - geometries-feature]]]]]] - [:circumstances - {:optional true - :description "Floorball information"} - circumstances-schema] - [:activities {:optional true} activities/activities-schema] - [:properties {:optional true} - (into [:map] (for [[k schema] prop-types/schemas] - [k {:optional true} schema]))]]) + [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]]]) + +(defn make-sports-site-schema [{:keys [title + type-codes + description + extras-schema + location-schema]}] + ;; TODO audit + [:and + #'sports-site-base + (mu/merge + [:map + {:title title + :description description} + [:type + [:map + [:type-code (into [:enum] type-codes)]]] + [:location location-schema]] + extras-schema)]) + +(def sports-site + (into [:multi {:title "SportsSite" + :dispatch (fn [x] + (-> x :type :type-code))}] + (for [[type-code {:keys [geometry-type props] :as x}] (sort-by key types/all) + :let [activity (get activities/by-type-code type-code) + activity-key (some-> activity :value keyword) + floorball? (= 2240 type-code)]] + [type-code (make-sports-site-schema + {:title (str type-code " - " (:fi (:name x))) + :type-codes #{type-code} + :location-schema (case geometry-type + "Point" #'point-location + "LineString" #'line-string-location + "Polygon" #'polygon-location) + :extras-schema (cond-> [:map] + (seq props) + (mu/assoc :properties + (into [:map] + (for [[k schema] (select-keys prop-types/schemas (keys props))] + [k {:optional true} schema])) + {:optional true}) + + floorball? + (mu/assoc :circumstances + circumstances-schema + {:optional true + :description "Floorball information"}) + + activity + (mu/assoc :activities + [:map + [activity-key + {:optional true} + (case activity-key + :outdoor-recreation-areas #'activities/outdoor-recreation-areas-schema + :outdoor-recreation-facilities #'activities/outdoor-recreation-facilities-schema + :outdoor-recreation-routes #'activities/outdoor-recreation-routes-schema + :cycling #'activities/cycling-schema + :paddling #'activities/paddling-schema + :birdwatching #'activities/birdwatching-schema + :fishing #'activities/fishing-schema)]] + {:optional true}))})]))) + -(comment - (json-schema/transform geometries-feature)) From 8635c8f9e4609fa71f468934242f51942fa806da Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 19 Dec 2024 15:02:52 +0200 Subject: [PATCH 05/13] WIP, add sports-sites search --- webapp/src/clj/lipas/backend/api/v2.clj | 60 +++++++++++++++- .../src/cljc/lipas/schema/sports_sites.cljc | 71 ++++++++++++------- 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 4bb16e42f..444dbf1f4 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -1,5 +1,7 @@ (ns lipas.backend.api.v2 - (:require [lipas.schema.sports-sites :as sports-sites-schema] + (:require [clojure.string :as str] + [lipas.backend.core :as core] + [lipas.schema.sports-sites :as sports-sites-schema] [reitit.coercion.malli] [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] @@ -13,8 +15,60 @@ :coercion reitit.coercion.malli/coercion} ["/sports-sites" - {:get {:handler (fn [_] - (resp/ok)) + {:get {:handler (fn [req] + (let [{:keys [type-codes city-codes]} (:query (:parameters req)) + query {:from 0 + :size 100 + :track_total_hits 50000 + :_source {:excludes ["search-meta.*"] + :includes ["lipas-id" + "status" + "name" + "name-localized" + "type.type-code" + "location.city.city-code"]} + :query {:bool {:must (cond-> [] + type-codes (conj {:terms {:type.type-code type-codes}}) + city-codes (conj {:terms {:location.city.city-code city-codes}}))}}} + x (core/search search query)] + (println (-> x + :body + :hits + :hits + ;; Why is there results with empty _source? + (->> (keep :_source)) + )) + (resp/ok + (-> x + :body + :hits + :hits + ;; Why is there results with empty _source? + (->> (keep :_source)) + )))) + :parameters {:query [:map + [:city-codes + {:optional true + :decode/string (fn [s] (str/split s #",")) + #_#_ + :description (->> cities/by-city-code + (sort-by key) + (map (fn [[code x]] + (str "* " code " - " (:fi (:name x)) ""))) + (str/join "\n") + (str "City-codes:\n"))} + #'sports-sites-schema/city-codes] + [:type-codes + {:optional true + :decode/string (fn [s] (str/split s #",")) + #_#_ + :description (->> types/all + (sort-by key) + (map (fn [[code x]] + (str "* " code " - " (:fi (:name x)) ""))) + (str/join "\n") + (str "Type-codes:\n"))} + #'sports-sites-schema/type-codes]]} :responses {200 {:body [:vector {:title "SportSites"} sports-sites-schema/sports-site]}}}}] diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index 454830fa1..990538309 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -8,7 +8,7 @@ [lipas.data.types :as types] [lipas.schema.core :as specs] [lipas.utils :as utils] - [malli.json-schema :as json-schema] + [malli.core :as m] [malli.util :as mu])) (def circumstances-schema @@ -72,6 +72,11 @@ [:available-goals-count {:optional true} :int] [:player-entrance {:optional true} :string]]) +(def city-code (into [:enum] (sort (keys cities/by-city-code)))) +(def city-codes [:set city-code]) + +(def type-codes [:set (into [:enum] (sort (keys types/all)))]) + ;; https://github.com/metosin/malli/issues/670 (def number-schema number?) @@ -79,7 +84,7 @@ [:map [:city [:map - [:city-code (into [:enum] (keys cities/by-city-code))] + [:city-code city-code] [:neighborhood {:optional true} [:string {:min 1 :max 100}]]]] [:address [:string {:min 1 :max 200}]] @@ -178,6 +183,7 @@ (def sports-site (into [:multi {:title "SportsSite" :dispatch (fn [x] + (println x (-> x :type :type-code)) (-> x :type :type-code))}] (for [[type-code {:keys [geometry-type props] :as x}] (sort-by key types/all) :let [activity (get activities/by-type-code type-code) @@ -192,31 +198,46 @@ "Polygon" #'polygon-location) :extras-schema (cond-> [:map] (seq props) - (mu/assoc :properties - (into [:map] - (for [[k schema] (select-keys prop-types/schemas (keys props))] - [k {:optional true} schema])) - {:optional true}) + (conj [:properties + {:optional true} + (into [:map] + (for [[k schema] (select-keys prop-types/schemas (keys props))] + [k {:optional true} schema]))]) floorball? - (mu/assoc :circumstances - circumstances-schema - {:optional true - :description "Floorball information"}) + (conj [:circumstances + {:optional true + :description "Floorball information"} + circumstances-schema]) activity - (mu/assoc :activities - [:map - [activity-key - {:optional true} - (case activity-key - :outdoor-recreation-areas #'activities/outdoor-recreation-areas-schema - :outdoor-recreation-facilities #'activities/outdoor-recreation-facilities-schema - :outdoor-recreation-routes #'activities/outdoor-recreation-routes-schema - :cycling #'activities/cycling-schema - :paddling #'activities/paddling-schema - :birdwatching #'activities/birdwatching-schema - :fishing #'activities/fishing-schema)]] - {:optional true}))})]))) - + (conj [:activities + {:optional true} + [:map + [activity-key + {:optional true} + (case activity-key + :outdoor-recreation-areas #'activities/outdoor-recreation-areas-schema + :outdoor-recreation-facilities #'activities/outdoor-recreation-facilities-schema + :outdoor-recreation-routes #'activities/outdoor-recreation-routes-schema + :cycling #'activities/cycling-schema + :paddling #'activities/paddling-schema + :birdwatching #'activities/birdwatching-schema + :fishing #'activities/fishing-schema)]]]))})]))) +(comment + (m/explain [:vector sports-site] + [{:lipas-id 1 + :status "active" + :name "foo" + :owner "city" + :admin "city-sports" + :location {:city {:city-code 5} + :address "foo" + :postal-code "00100" + :postal-office "foo" + :geometries {:type "FeatureCollection" + :features [{:type "Feature" + :geometry {:type "Point" + :coordinates [0.0 0.0]}}]}} + :type {:type-code 1530}}])) From aff02d1b3bc43322683f9d1456af524873313857 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 20 Dec 2024 15:11:54 +0200 Subject: [PATCH 06/13] Fix response coercion --- webapp/project.clj | 2 +- webapp/src/clj/lipas/backend/api/v2.clj | 38 ++++++----------- .../src/cljc/lipas/schema/sports_sites.cljc | 41 ++++++++++--------- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/webapp/project.clj b/webapp/project.clj index 62ea137ef..41db25d2f 100644 --- a/webapp/project.clj +++ b/webapp/project.clj @@ -9,7 +9,7 @@ [metosin/reitit "0.7.1"] [metosin/ring-swagger-ui "5.18.2"] [metosin/spec-tools "0.10.7"] - [metosin/malli "0.16.2"] + [metosin/malli "0.17.0"] ;;; Frontend ;;; [thheller/shadow-cljs "2.28.16"] diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 444dbf1f4..b5f85b73a 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -20,32 +20,19 @@ query {:from 0 :size 100 :track_total_hits 50000 - :_source {:excludes ["search-meta.*"] - :includes ["lipas-id" - "status" - "name" - "name-localized" - "type.type-code" - "location.city.city-code"]} + :_source {:excludes ["search-meta.*"]} :query {:bool {:must (cond-> [] type-codes (conj {:terms {:type.type-code type-codes}}) city-codes (conj {:terms {:location.city.city-code city-codes}}))}}} x (core/search search query)] - (println (-> x - :body - :hits - :hits - ;; Why is there results with empty _source? - (->> (keep :_source)) - )) - (resp/ok - (-> x - :body - :hits - :hits - ;; Why is there results with empty _source? - (->> (keep :_source)) - )))) + {:status 200 + :body {:items (-> x + :body + :hits + :hits + ;; Why is there results with empty _source? + (->> (keep :_source)) + )}})) :parameters {:query [:map [:city-codes {:optional true @@ -69,9 +56,10 @@ (str/join "\n") (str "Type-codes:\n"))} #'sports-sites-schema/type-codes]]} - :responses {200 {:body [:vector - {:title "SportSites"} - sports-sites-schema/sports-site]}}}}] + :responses {200 {:body [:map + [:items [:vector + {:title "SportSites"} + sports-sites-schema/sports-site]]]}}}}] ["/lois" {:get {:handler (fn [_] (resp/ok))}}] diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index 990538309..af6b58212 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -133,7 +133,9 @@ (def sports-site-base [:map - {:title "Shared Properties"} + {:title "Shared Properties" + ;; Because this is used from :and, both branches need to be open for Malli to work. + :closed false} [:lipas-id [:int]] [:status (into [:enum] (keys sports-sites/statuses))] [:name [:string {:min 2 :max 100}]] @@ -173,7 +175,8 @@ (mu/merge [:map {:title title - :description description} + :description description + :closed false} [:type [:map [:type-code (into [:enum] type-codes)]]] @@ -183,8 +186,8 @@ (def sports-site (into [:multi {:title "SportsSite" :dispatch (fn [x] - (println x (-> x :type :type-code)) - (-> x :type :type-code))}] + (-> x :type :type-code))} + [:malli.core/default :any]] (for [[type-code {:keys [geometry-type props] :as x}] (sort-by key types/all) :let [activity (get activities/by-type-code type-code) activity-key (some-> activity :value keyword) @@ -226,18 +229,18 @@ :fishing #'activities/fishing-schema)]]]))})]))) (comment - (m/explain [:vector sports-site] - [{:lipas-id 1 - :status "active" - :name "foo" - :owner "city" - :admin "city-sports" - :location {:city {:city-code 5} - :address "foo" - :postal-code "00100" - :postal-office "foo" - :geometries {:type "FeatureCollection" - :features [{:type "Feature" - :geometry {:type "Point" - :coordinates [0.0 0.0]}}]}} - :type {:type-code 1530}}])) + (m/validate [:vector sports-site] + [{:lipas-id 1 + :status "active" + :name "foo" + :owner "city" + :admin "city-sports" + :location {:city {:city-code 5} + :address "foo" + :postal-code "00100" + :postal-office "foo" + :geometries {:type "FeatureCollection" + :features [{:type "Feature" + :geometry {:type "Point" + :coordinates [0.0 0.0]}}]}} + :type {:type-code 1530}}])) From 878b3e51186eff275a32d54ac372ea9bea9c9c0a Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 20 Dec 2024 15:14:02 +0200 Subject: [PATCH 07/13] Hide v2 routes from regular swagger spec --- webapp/src/clj/lipas/backend/api/v2.clj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index b5f85b73a..311ef810e 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -12,6 +12,9 @@ {:url "/api-v2/openapi.json"})] ["/api-v2" {:openapi {:id :api-v2} + ;; The regular handle is still using swagger-spec, so :openapi :id doesn't hide + ;; these routes from that. + :swagger {:id :hide-from-default} :coercion reitit.coercion.malli/coercion} ["/sports-sites" From f8166525ea353c8d79a3adfd2fa015790ef1d761 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Mon, 23 Dec 2024 16:57:34 +0200 Subject: [PATCH 08/13] Implement Loi endpoint --- .github/workflows/ci.yaml | 6 +- webapp/src/clj/lipas/backend/api/v2.clj | 246 ++++++++++++++---- webapp/src/clj/lipas/backend/core.clj | 16 +- webapp/src/cljc/lipas/data/activities.cljc | 139 ++++------ webapp/src/cljc/lipas/data/loi.cljc | 118 +-------- webapp/src/cljc/lipas/schema/common.cljc | 82 ++++++ webapp/src/cljc/lipas/schema/core.cljc | 5 +- webapp/src/cljc/lipas/schema/lois.cljc | 118 +++++++++ .../src/cljc/lipas/schema/sports_sites.cljc | 201 +++++--------- .../lipas/schema/sports_sites/activities.cljc | 13 + .../schema/sports_sites/circumstances.cljc | 63 +++++ .../test/clj/lipas/backend/handler_test.clj | 3 +- 12 files changed, 611 insertions(+), 399 deletions(-) create mode 100644 webapp/src/cljc/lipas/schema/common.cljc create mode 100644 webapp/src/cljc/lipas/schema/lois.cljc create mode 100644 webapp/src/cljc/lipas/schema/sports_sites/activities.cljc create mode 100644 webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d417593cc..295dcf69f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -173,16 +173,16 @@ jobs: uses: tj-actions/changed-files@v41 with: files: | - webapp/src/cljc/lipas/data/loi.cljc + webapp/src/cljc/lipas/schema/lois.cljc - name: Generate JSON schema if: steps.changed-files.outputs.any_changed == 'true' - run: lein run -m lipas.data.loi > loi-schema.json + run: lein run -m lipas.schema.lois > loi-schema.json working-directory: ./webapp - name: Generate CSV if: steps.changed-files.outputs.any_changed == 'true' - run: lein run -m lipas.data.loi csv > loi-schema.csv + run: lein run -m lipas.schema.lois csv > loi-schema.csv working-directory: ./webapp - name: Deploy JSON schema to Gist diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 311ef810e..e06afc0d4 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -1,12 +1,89 @@ (ns lipas.backend.api.v2 (:require [clojure.string :as str] [lipas.backend.core :as core] + [lipas.schema.common :as common-schema] + [lipas.schema.lois :as lois-schema] [lipas.schema.sports-sites :as sports-sites-schema] + [lipas.schema.sports-sites.activities :as activities-schema] [reitit.coercion.malli] [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] [ring.util.http-response :as resp])) +(def sports-site-keys + "Publicly exposed top level keys." + ["lipas-id" + "status" + "event-date" + "admin" + "name" + "marketing-name" + "name-localized" + "construction-year" + "renovation-years" + "owner" + "location" + "properties" + "www" + "phone-number" + "type" + "activities" + "comment" + "circumstances"]) + +(defn decode-heisenparam + "Coerces singular and repeating params into a collection. Singular + query param comes in as scalar, while multiple params come in as + vector. Handles also possibly comma-separated vals." + [x] + (cond-> x + (string? x) (str/split #","))) + +(def pagination-schema + [:map + [:current-page [:int {:min 1}]] + [:page-size [:int {:min 1}]] + [:total-items [:int {:min 0}]] + [:total-pages [:int {:min 0}]]]) + +(defn ->pagination + [{:keys [page page-size total-items] + :or {page 1 page-size 10}}] + {:current-page page + :page-size page-size + :total-items total-items + :total-pages (-> total-items (/ page-size) Math/ceil int)}) + +(defn ->sports-sites-query + [{:keys [page page-size statuses type-codes city-codes admins owners activities] + :or {page-size 10}}] + {:from (* (dec page) page-size) + :size page-size + :track_total_hits 50000 + :_source {:includes sports-site-keys} + :query {:bool + {:must (cond-> [] + statuses (conj {:terms {:status.keyword statuses}}) + type-codes (conj {:terms {:type.type-code type-codes}}) + city-codes (conj {:terms {:location.city.city-code city-codes}}) + admins (conj {:terms {:admin.keyword admins}}) + owners (conj {:terms {:owner.keyword owners}}) + activities (into (for [a activities] + {:exists {:field (str "activities." a)}})))}}}) + +(defn ->lois-query + [{:keys [page page-size statuses types categories] + :or {page-size 10}}] + {:from (* (dec page) page-size) + :size page-size + :track_total_hits 50000 + #_#_:_source {:includes sports-site-keys} + :query {:bool + {:must (cond-> [] + statuses (conj {:terms {:status.keyword statuses}}) + types (conj {:terms {:loi-type.keyword types}}) + categories (conj {:terms {:loi-category.keyword categories}}))}}}) + (defn routes [{:keys [db search] :as ctx}] (let [ui-handler (swagger-ui/create-swagger-ui-handler {:url "/api-v2/openapi.json"})] @@ -18,54 +95,129 @@ :coercion reitit.coercion.malli/coercion} ["/sports-sites" - {:get {:handler (fn [req] - (let [{:keys [type-codes city-codes]} (:query (:parameters req)) - query {:from 0 - :size 100 - :track_total_hits 50000 - :_source {:excludes ["search-meta.*"]} - :query {:bool {:must (cond-> [] - type-codes (conj {:terms {:type.type-code type-codes}}) - city-codes (conj {:terms {:location.city.city-code city-codes}}))}}} - x (core/search search query)] - {:status 200 - :body {:items (-> x - :body - :hits - :hits - ;; Why is there results with empty _source? - (->> (keep :_source)) - )}})) - :parameters {:query [:map - [:city-codes - {:optional true - :decode/string (fn [s] (str/split s #",")) - #_#_ - :description (->> cities/by-city-code - (sort-by key) - (map (fn [[code x]] - (str "* " code " - " (:fi (:name x)) ""))) - (str/join "\n") - (str "City-codes:\n"))} - #'sports-sites-schema/city-codes] - [:type-codes - {:optional true - :decode/string (fn [s] (str/split s #",")) - #_#_ - :description (->> types/all - (sort-by key) - (map (fn [[code x]] - (str "* " code " - " (:fi (:name x)) ""))) - (str/join "\n") - (str "Type-codes:\n"))} - #'sports-sites-schema/type-codes]]} - :responses {200 {:body [:map - [:items [:vector - {:title "SportSites"} - sports-sites-schema/sports-site]]]}}}}] + {:tags ["sports-sites"] + :get + {:handler + (fn [req] + (tap> (:parameters req)) + (let [params (:query (:parameters req)) + query (->sports-sites-query params) + _ (tap> query) + results (core/search search query)] + (tap> results) + {:status 200 + :body + {:items (-> results :body :hits :hits (->> (keep :_source))) + :pagination (let [total-items (-> results :body :hits :total :value)] + (->pagination (assoc params :total-items total-items)))}})) + + :parameters {:query + [:map + + [:page {:optional true} + [:int {:min 1 :json-schema/default 1}]] + + [:page-size {:optional true} + [:int {:min 1 :max 100 :json-schema/default 10}]] + + [:statuses + {:optional true + :decode/string decode-heisenparam + :json-schema/default #{"active" "out-of-service-temporarily"}} + #'common-schema/statuses] + + [:city-codes + {:optional true + :decode/string decode-heisenparam + #_#_ + :description (->> cities/by-city-code + (sort-by key) + (map (fn [[code x]] + (str "* " code " - " (:fi (:name x)) ""))) + (str/join "\n") + (str "City-codes:\n"))} + #'sports-sites-schema/city-codes] + + [:type-codes + {:optional true + :decode/string decode-heisenparam + #_#_ + :description (->> types/all + (sort-by key) + (map (fn [[code x]] + (str "* " code " - " (:fi (:name x)) ""))) + (str/join "\n") + (str "Type-codes:\n"))} + #'sports-sites-schema/type-codes] + + [:admins + {:optional true + :decode/string decode-heisenparam} + #'sports-sites-schema/admins] + + [:owners + {:optional true + :decode/string decode-heisenparam} + #'sports-sites-schema/owners] + + [:activities + {:optional true + :decode/string decode-heisenparam} + #'activities-schema/activities]]} + + :responses {200 {:body [:map + [:items + [:vector {:title "SportSites"} + #'sports-sites-schema/sports-site]] + [:pagination {:title "Pagination"} + pagination-schema]]}}}}] + ["/lois" - {:get {:handler (fn [_] - (resp/ok))}}] + {:tags ["locations-of-interest"] + :get {:handler (fn [req] + (tap> (:parameters req)) + (let [params (:query (:parameters req)) + query (->lois-query params) + _ (tap> query) + results (core/search-lois* search query)] + (tap> results) + {:status 200 + :body + {:items (-> results :body :hits :hits (->> (keep :_source))) + :pagination (let [total-items (-> results :body :hits :total :value)] + (->pagination (assoc params :total-items total-items)))}})) + + :parameters {:query + [:map + + [:page {:optional true} + [:int {:min 1 :json-schema/default 1}]] + + [:page-size {:optional true} + [:int {:min 1 :max 100 :json-schema/default 10}]] + + [:statuses + {:optional true + :decode/string decode-heisenparam + :json-schema/default #{"active" "out-of-service-temporarily"}} + #'common-schema/statuses] + + [:categories + {:optional true + :decode/string decode-heisenparam} + #'lois-schema/loi-categories] + + [:types + {:optional true + :decode/string decode-heisenparam} + #'lois-schema/loi-types]]} + + :responses {200 {:body [:map + [:items + [:vector {:title "LocationsOfInterest"} + #'lois-schema/loi]] + [:pagination {:title "Pagination"} + pagination-schema]]}}}}] ["/openapi.json" {:get diff --git a/webapp/src/clj/lipas/backend/core.clj b/webapp/src/clj/lipas/backend/core.clj index 1d0988273..63e38fb8e 100644 --- a/webapp/src/clj/lipas/backend/core.clj +++ b/webapp/src/clj/lipas/backend/core.clj @@ -875,14 +875,18 @@ query default-query))) -(defn search-lois +(defn search-lois* [{:keys [indices client]} es-query] (let [idx-name (get-in indices [:lois :search])] - (-> (search/search client idx-name es-query) - :body - :hits - :hits - (->> (map :_source))))) + (search/search client idx-name es-query))) + +(defn search-lois + [search es-query] + (-> (search-lois* search es-query) + :body + :hits + :hits + (->> (map :_source)))) (defn get-loi [{:keys [indices client]} loi-id] diff --git a/webapp/src/cljc/lipas/data/activities.cljc b/webapp/src/cljc/lipas/data/activities.cljc index 1246f8d3b..d50f9ffb9 100644 --- a/webapp/src/cljc/lipas/data/activities.cljc +++ b/webapp/src/cljc/lipas/data/activities.cljc @@ -4,6 +4,7 @@ #?(:clj [clojure.data.csv :as csv]) #?(:clj [clojure.string :as str]) [lipas.data.materials :as materials] + [lipas.schema.common :as common-schema] [lipas.utils :as utils] [malli.core :as m] [malli.json-schema :as json-schema] @@ -12,47 +13,15 @@ (defn collect-schema [m] (into [:map] (map (juxt first (constantly {:optional true}) (comp :schema second)) m))) - -(def localized-string-schema - [:map - [:fi {:optional true} [:string]] - [:se {:optional true} [:string]] - [:en {:optional true} [:string]]]) - -(def number-schema - [:or [:int] [:double]]) - -(def percentage-schema - (let [props {:min 0 :max 100}] - [:or [:int props] [:double props]])) - (def duration-schema [:map - [:min {:optional true} number-schema] - [:max {:optional true} number-schema] + [:min {:optional true} common-schema/number] + [:max {:optional true} common-schema/number] [:unit {:optional true} [:enum "days" "hours" "minutes"]]]) (def surface-material-schema [:sequential (into [:enum] (keys materials/surface-materials))]) -(def route-fcoll-schema - [:map - [:type [:enum "FeatureCollection"]] - [:features - [:sequential - [:map - [:id {:optional true} [:string]] - [:type [:enum "Feature"]] - [:properties {:optional true} [:map]] - [:geometry - [:map - [:type [:enum "LineString"]] - [:coordinates - [:sequential - [:or - [:tuple :double :double] - [:tuple :double :double :double]]]]]]]]]]) - (def contact-roles {"admin" {:fi "Ylläpitäjä" :se "Administratör" @@ -207,8 +176,8 @@ [:custom-rules {:optional true} [:sequential [:map - [:label {:optional true} localized-string-schema] - [:description {:optional true} localized-string-schema] + [:label {:optional true} common-schema/localized-string] + [:description {:optional true} common-schema/localized-string] [:value {:optional true} [:string {:min 2}]]]]]]) (def status-opts @@ -236,7 +205,7 @@ :opts status-opts}} :description-short - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "1-3 lauseen esittely kohteesta ja sen erityispiirteistä." @@ -247,7 +216,7 @@ :en "Overview"}}} :description-long - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Yleiskuvausta jatkava, laajempi kuvaus kohteesta ja sen ominaisuuksista" @@ -260,11 +229,11 @@ :contacts {:schema [:sequential [:map - [:organization {:optional true} localized-string-schema] + [:organization {:optional true} common-schema/localized-string] [:role {:optional true} [:sequential (into [:enum] (keys contact-roles))]] - [:email {:optional true} localized-string-schema] - [:www {:optional true} localized-string-schema] - [:phone-number {:optional true} localized-string-schema]]] + [:email {:optional true} common-schema/localized-string] + [:www {:optional true} common-schema/localized-string] + [:phone-number {:optional true} common-schema/localized-string]]] :field {:type "contacts" :description {:fi "Syötä kohteesta vastaavien tahojen yhteystiedot" @@ -342,7 +311,7 @@ {:schema [:sequential [:map [:url [:string]] - [:description {:optional true} localized-string-schema]]] + [:description {:optional true} common-schema/localized-string]]] :field {:type "videos" :description {:fi "Lisää URL-linkki web-palvelussa olevaan kohteen maisemia, luontoa tai harrastamisen olosuhteita esittelevään videoon. Varmista, että sinulla on oikeus lisätä video." @@ -356,9 +325,9 @@ {:schema [:sequential [:map [:url [:string]] - [:description {:optional true} localized-string-schema] - [:alt-text {:optional true} localized-string-schema] - [:copyright {:optional true} localized-string-schema]]] + [:description {:optional true} common-schema/localized-string] + [:alt-text {:optional true} common-schema/localized-string] + [:copyright {:optional true} common-schema/localized-string]]] :field {:type "images" :description {:fi "Lisää kohteen maisemia, luontoa tai harrastamisen olosuhteita esitteleviä valokuvia. Voit lisätä vain kuvatiedostoja, et URL-kuvalinkkejä. Kelvollisia tiedostomuotoja ovat .jpg, .jpeg ja .png. Varmista, että sinulla on oikeus lisätä kuva." @@ -401,7 +370,7 @@ :en "Copyright information"}}}}}} :additional-info-link - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "text-field" :description {:fi "Linkki ulkoisella sivustolla sijaitsevaan laajempaan kohde-esittelyyn" @@ -412,7 +381,7 @@ :en "More information"}}} :rules - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Liikkumis- tai toimintaohjeet, joiden avulla ohjataan toimintaa. Tässä voidaan kertoa myös mahdollisista liikkumis- tai toimintarajoituksista." @@ -423,7 +392,7 @@ :en "Permits, regulations, instructions"}}} :arrival - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Eri kulkumuodoilla kohteeseen pääsyyn liittyvää tietoa. Esim. pysäköintialueet ja joukkoliikenneyhteydet." @@ -434,7 +403,7 @@ :en "Arrival to destination"}}} :accessibility - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Yleistä tietoa kohteen esteettömyydestä tai kuljettavuudesta" @@ -445,7 +414,7 @@ :en "Accessibility"}}} :highlights - {:schema [:sequential localized-string-schema] + {:schema [:sequential common-schema/localized-string] :field {:type "textlist" :description {:fi "Syötä 2-6 konkreettista kohteen erityispiirrettä, jotka täydentävät yleiskuvausta. Syötä yksi kohokohta kerrallaan. Käytä isoa Alkukirjainta." @@ -638,21 +607,21 @@ (mu/dissoc :rules)) [:map [:id [:string]] - [:geometries route-fcoll-schema] + [:geometries common-schema/line-string-fcoll] [:accessibility-categorized {:optional true} [:map - [:mobility-impaired {:optional true} localized-string-schema] - [:hearing-impaired {:optional true} localized-string-schema] - [:visually-impaired {:optional true} localized-string-schema] - [:developmentally-disabled {:optional true} localized-string-schema]]] - [:route-name {:optional true} localized-string-schema] + [:mobility-impaired {:optional true} common-schema/localized-string] + [:hearing-impaired {:optional true} common-schema/localized-string] + [:visually-impaired {:optional true} common-schema/localized-string] + [:developmentally-disabled {:optional true} common-schema/localized-string]]] + [:route-name {:optional true} common-schema/localized-string] [:outdoor-recreation-activities {:optional true} [:sequential (into [:enum] (keys outdoor-recreation-routes-activities))]] [:duration {:optional true} duration-schema] [:travel-direction {:optional true} [:enum "clockwise" "counter-clockwise"]] - [:route-marking {:optional true} localized-string-schema] + [:route-marking {:optional true} common-schema/localized-string] [:rules-structured {:optional true} rules-structured-schema] - [:route-length-km {:optional true} number-schema] + [:route-length-km {:optional true} common-schema/number] [:surface-material {:optional true} surface-material-schema] [:accessibility-classification {:optional true} (into [:enum] (keys accessibility-classification))] @@ -826,7 +795,7 @@ :en "Select all the surface materials on the route"}}} #_#_:latest-updates - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Tähän joku seliteteksti" @@ -969,23 +938,23 @@ common-route-props-schema [:map [:id [:string]] - [:geometries route-fcoll-schema] - [:route-name {:optional true} localized-string-schema] + [:geometries common-schema/line-string-fcoll] + [:route-name {:optional true} common-schema/localized-string] [:cycling-activities {:optional true} [:sequential (into [:enum] (keys cycling-activities))]] [:cycling-difficulty {:optional true} (into [:enum] (keys cycling-difficulty))] - [:cycling-route-difficulty {:optional true} localized-string-schema] + [:cycling-route-difficulty {:optional true} common-schema/localized-string] [:duration {:optional true} duration-schema] - [:food-and-water {:optional true} localized-string-schema] - [:accommodation {:optional true} localized-string-schema] - [:good-to-know {:optional true} localized-string-schema] - [:route-notes {:optional true} localized-string-schema] - [:route-length-km {:optional true} number-schema] + [:food-and-water {:optional true} common-schema/localized-string] + [:accommodation {:optional true} common-schema/localized-string] + [:good-to-know {:optional true} common-schema/localized-string] + [:route-notes {:optional true} common-schema/localized-string] + [:route-length-km {:optional true} common-schema/number] [:surface-material {:optional true} surface-material-schema] - [:unpaved-percentage {:optional true} percentage-schema] - [:trail-percentage {:optional true} percentage-schema] - [:cyclable-percentage {:optional true} percentage-schema] + [:unpaved-percentage {:optional true} common-schema/percentage] + [:trail-percentage {:optional true} common-schema/percentage] + [:cyclable-percentage {:optional true} common-schema/percentage] pilgrimage-key-schema])] :field {:type "routes" @@ -1305,8 +1274,8 @@ common-route-props-schema [:map [:id [:string]] - [:geometries route-fcoll-schema] - [:route-name {:optional true} localized-string-schema] + [:geometries common-schema/line-string-fcoll] + [:route-name {:optional true} common-schema/localized-string] [:paddling-activities {:optional true} [:sequential (into [:enum] (keys paddling-activities))]] [:paddling-route-type {:optional true} @@ -1315,8 +1284,8 @@ [:sequential (into [:enum] (keys paddling-properties))]] [:paddling-difficulty (into [:enum] (keys paddling-difficulty))] [:travel-direction {:optional true} [:enum "clockwise" "counter-clockwise"]] - [:safety {:optional true} localized-string-schema] - [:good-to-know {:optional true} localized-string-schema] + [:safety {:optional true} common-schema/localized-string] + [:good-to-know {:optional true} common-schema/localized-string] [:duration {:optional true} duration-schema] pilgrimage-key-schema])] :field @@ -1491,7 +1460,7 @@ :opts birdwatching-types}} :birdwatching-habitat - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Suokohde, …" @@ -1502,7 +1471,7 @@ :en "Habitat"}}} :birdwatching-character - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Muutonseurantakohde, …" @@ -1525,7 +1494,7 @@ :opts birdwatching-seasons}} :birdwatching-species - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Kahlaajat, Vesilinnut, Petolinnut, …" @@ -1651,7 +1620,7 @@ :opts fishing-species}} :fish-population - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Kirjoita tähän kuvaus kohteen vesistössä esiintyvästä kalastosta." @@ -1663,7 +1632,7 @@ :fishing-methods - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Tietoa mm. kohteessa kalastukseen vaikuttavista erityispiirteistä, toimivista välinevalinnoista yms." @@ -1686,7 +1655,7 @@ :opts fishing-permit-opts}} :fishing-permit-additional-info - {:schema localized-string-schema + {:schema common-schema/localized-string :field {:type "textarea" :description {:fi "Syötä tähän tarvittaessa lisätietoa kalastuslupia koskevista muista asioista" @@ -1710,10 +1679,10 @@ :accessibility-categorized {:schema [:map - [:mobility-impaired {:optional true} localized-string-schema] - [:hearing-impaired {:optional true} localized-string-schema] - [:visually-impaired {:optional true} localized-string-schema] - [:developmentally-disabled {:optional true} localized-string-schema]] + [:mobility-impaired {:optional true} common-schema/localized-string] + [:hearing-impaired {:optional true} common-schema/localized-string] + [:visually-impaired {:optional true} common-schema/localized-string] + [:developmentally-disabled {:optional true} common-schema/localized-string]] :field {:type "accessibility" diff --git a/webapp/src/cljc/lipas/data/loi.cljc b/webapp/src/cljc/lipas/data/loi.cljc index 5150e9257..ab2e5a70a 100644 --- a/webapp/src/cljc/lipas/data/loi.cljc +++ b/webapp/src/cljc/lipas/data/loi.cljc @@ -1,9 +1,5 @@ (ns lipas.data.loi - (:require - #?(:clj [cheshire.core :as json]) - #?(:clj [clojure.data.csv :as csv]) - [lipas.data.status :as status] - [malli.json-schema :as json-schema])) + (:require [lipas.data.status :as status])) (def statuses status/statuses) @@ -443,119 +439,7 @@ :geom-type "Polygon" :props (merge common-props protected-area-fields)}}}}) -(def point-fcoll-schema - [:map - [:type [:enum "FeatureCollection"]] - [:features - [:sequential - [:map - [:id {:optional true} [:string]] - [:type [:enum "Feature"]] - [:properties {:optional true} [:map]] - [:geometry - [:map - [:type [:enum "Point"]] - [:coordinates - [:or - [:tuple :double :double] - [:tuple :double :double :double]]]]]]]]]) - -(def schema - (into [:or] - (for [[cat-k cat-v] categories - [_type-k type-v] (:types cat-v)] - (into - [:map {:description (str cat-k " > " (:value type-v)) - :title (-> type-v :label :fi)} - [:id [:string]] - [:event-date [:string]] - [:created-at [:string]] - [:geometries point-fcoll-schema] - [:status (into [:enum] (keys statuses))] - [:loi-category [:enum cat-k]] - [:loi-type [:enum (:value type-v)]]] - (for [[prop-k prop-v] (:props type-v)] - [prop-k {:optional true} (:schema prop-v)]))))) - -(defn gen-json-schema - [] - (-> schema - json-schema/transform - #?(:clj(json/encode {:pretty true}) - :cljs clj->js) - println)) - -(declare gen-csv) - -#?(:clj - (defn gen-csv - [] - (->> - (for [[category-code category] categories - [_ type] (:types category) - [prop-k prop] (:props type)] - [category-code - (get-in category [:label :fi]) - (get-in category [:label :se]) - (get-in category [:label :en]) - (get-in category [:description :fi]) - (get-in category [:description :se]) - (get-in category [:description :en]) - (:value type) - (get-in type [:label :fi]) - (get-in type [:label :se]) - (get-in type [:label :en]) - (get-in type [:description :fi]) - (get-in type [:description :se]) - (get-in type [:description :en]) - (name prop-k) - (get-in prop [:field :label :fi]) - (get-in prop [:field :label :se]) - (get-in prop [:field :label :en]) - (get-in prop [:field :description :fi]) - (get-in prop [:field :description :se]) - (get-in prop [:field :description :en])]) - (into [["kategoria" - "kategoria nimi fi" - "kategoria nimi se" - "kategoria nimi en" - "kategoria kuvaus fi" - "kategoria kuvaus se" - "kategoria kuvaus en" - "tyyppi" - "tyyppi nimi fi" - "tyyppi nimi se" - "tyyppi nimi en" - "tyyppi kuvaus fi" - "tyyppi kuvaus se" - "tyyppi kuvaus en" - "ominaisuus" - "ominaisuus nimi fi" - "ominaisuus nimi se" - "ominaisuus nimi en" - "ominaisuus kuvaus fi" - "ominaisuus kuvaus se" - "ominaisuus kuvaus en"]]) - (csv/write-csv *out*)))) - -(comment - (gen-json-schema) - - (json-schema/transform [:tuple :double :double]) - ;; => {:type "array", - ;; :items [{:type "number"} {:type "number"}], - ;; :additionalItems false} - - - (gen-csv) - ) - (def types (->> categories vals (mapcat :types) (into {}))) - -(defn -main [& args] - (if (= "csv" (first args)) - (gen-csv) - (gen-json-schema))) diff --git a/webapp/src/cljc/lipas/schema/common.cljc b/webapp/src/cljc/lipas/schema/common.cljc new file mode 100644 index 000000000..209b362b8 --- /dev/null +++ b/webapp/src/cljc/lipas/schema/common.cljc @@ -0,0 +1,82 @@ +(ns lipas.schema.common + (:require [lipas.data.status :as status])) + +(def status (into [:enum] (keys status/statuses))) +(def statuses [:set status]) +;; https://github.com/metosin/malli/issues/670 +(def number number?) +(def percentage [number? {:min 0 :max 100}]) + +(def localized-string + [:map + [:fi {:optional true} [:string]] + [:se {:optional true} [:string]] + [:en {:optional true} [:string]]]) + +(def coordinates + [:vector {:min 2 + :max 3 + :title "Coordinates" + :description "WGS84 Lon, Lat and optional altitude in meters"} + number?]) + +(def point-geometry + [:map + {:title "Point"} + [:type [:enum "Point"]] + [:coordinates #'coordinates]]) + +(def line-string-geometry + [:map + {:title "LineString"} + [:type [:enum "LineString"]] + [:coordinates [:vector #'coordinates]]]) + +(def polygon-geometry + [:map + {:title "Polygon"} + [:type [:enum "Polygon"]] + [:coordinates [:vector [:vector #'coordinates]]]]) + +(def point-feature + [:map {:title "PointFeature"} + [:type [:enum "Feature"]] + [:geometry #'point-geometry] + [:properties {:optional true} [:map]]]) + +(def line-string-feature + [:map {:title "LineStringFeature"} + [:type [:enum "Feature"]] + [:geometry #'line-string-geometry] + [:properties {:optional true} [:map]]]) + +(def polygon-feature + [:map {:title "PolygonFeature"} + [:type [:enum "Feature"]] + [:geometry #'polygon-geometry] + [:properties {:optional true} [:map]]]) + +(def point-fcoll + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:sequential point-feature]]]) + +(def line-string-fcoll + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:sequential + #'line-string-feature]]]) + +(def polygon-fcoll + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:sequential + #'polygon-feature]]]) + +(comment + (require '[malli.core :as m]) + (m/schema point-geometry) + (m/schema line-string-geometry)) diff --git a/webapp/src/cljc/lipas/schema/core.cljc b/webapp/src/cljc/lipas/schema/core.cljc index 49fca3b0f..d53685879 100644 --- a/webapp/src/cljc/lipas/schema/core.cljc +++ b/webapp/src/cljc/lipas/schema/core.cljc @@ -16,6 +16,7 @@ [lipas.data.prop-types :as prop-types] [lipas.data.reminders :as reminders] [lipas.data.sports-sites :as sports-sites] + [lipas.data.status :as status] [lipas.data.swimming-pools :as swimming-pools] [lipas.data.types :as sports-site-types] [lipas.reports :as reports] @@ -2245,9 +2246,9 @@ (s/def :lipas.loi/event-date :lipas/timestamp) (s/def :lipas.loi/status - (st/spec {:spec (into #{} (keys loi/statuses)) + (st/spec {:spec (into #{} (keys status/statuses)) :swagger/type "string" - :swagger/enum (keys loi/statuses)})) + :swagger/enum (keys status/statuses)})) (s/def :lipas.loi/loi-category (st/spec {:spec (into #{} (keys loi/categories)) diff --git a/webapp/src/cljc/lipas/schema/lois.cljc b/webapp/src/cljc/lipas/schema/lois.cljc new file mode 100644 index 000000000..6662e3bfa --- /dev/null +++ b/webapp/src/cljc/lipas/schema/lois.cljc @@ -0,0 +1,118 @@ +(ns lipas.schema.lois + (:require #?(:clj [cheshire.core :as json]) + #?(:clj [clojure.data.csv :as csv]) + [lipas.data.loi :as loi] + [lipas.schema.common :as common] + [malli.json-schema :as json-schema])) + +(def loi-category (into [:enum] (keys loi/categories))) +(def loi-categories [:set loi-category]) +(def loi-type (into [:enum] (for [[_ category] loi/categories + [_ type] (:types category)] + (:value type)))) +(def loi-types [:set loi-type]) + +(def loi + (into [:multi {:title "LocationOfInterest" + :dispatch :loi-type}] + (for [[cat-k cat-v] loi/categories + [_type-k type-v] (:types cat-v)] + [(:value type-v) + (into + [:map {:description (str cat-k " > " (:value type-v)) + :title (-> type-v :label :fi)} + [:id [:string]] + [:event-date [:string]] + #_[:created-at [:string]] + [:geometries (case (:geom-type type-v) + ("Polygon") #'common/polygon-fcoll + ("LineString") #'common/line-string-fcoll + #'common/point-fcoll)] + [:status common/status] + [:loi-category [:enum cat-k]] + [:loi-type [:enum (:value type-v)]]] + (for [[prop-k prop-v] (:props type-v)] + [prop-k {:optional true} (:schema prop-v)]))]))) + +(comment + (require '[malli.core :as m]) + (m/schema loi) + ) + +(defn gen-json-schema + [] + (-> loi + json-schema/transform + #?(:clj(json/encode {:pretty true}) + :cljs clj->js) + println)) + +(declare gen-csv) + +#?(:clj + (defn gen-csv + [] + (->> + (for [[category-code category] loi/categories + [_ type] (:types category) + [prop-k prop] (:props type)] + [category-code + (get-in category [:label :fi]) + (get-in category [:label :se]) + (get-in category [:label :en]) + (get-in category [:description :fi]) + (get-in category [:description :se]) + (get-in category [:description :en]) + (:value type) + (get-in type [:label :fi]) + (get-in type [:label :se]) + (get-in type [:label :en]) + (get-in type [:description :fi]) + (get-in type [:description :se]) + (get-in type [:description :en]) + (name prop-k) + (get-in prop [:field :label :fi]) + (get-in prop [:field :label :se]) + (get-in prop [:field :label :en]) + (get-in prop [:field :description :fi]) + (get-in prop [:field :description :se]) + (get-in prop [:field :description :en])]) + (into [["kategoria" + "kategoria nimi fi" + "kategoria nimi se" + "kategoria nimi en" + "kategoria kuvaus fi" + "kategoria kuvaus se" + "kategoria kuvaus en" + "tyyppi" + "tyyppi nimi fi" + "tyyppi nimi se" + "tyyppi nimi en" + "tyyppi kuvaus fi" + "tyyppi kuvaus se" + "tyyppi kuvaus en" + "ominaisuus" + "ominaisuus nimi fi" + "ominaisuus nimi se" + "ominaisuus nimi en" + "ominaisuus kuvaus fi" + "ominaisuus kuvaus se" + "ominaisuus kuvaus en"]]) + (csv/write-csv *out*)))) + +(comment + (gen-json-schema) + + (json-schema/transform [:tuple :double :double]) + ;; => {:type "array", + ;; :items [{:type "number"} {:type "number"}], + ;; :additionalItems false} + + + (gen-csv) + ) + +(defn -main [& args] + (if (= "csv" (first args)) + (gen-csv) + (gen-json-schema))) diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index af6b58212..c5b2d9e4a 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -1,90 +1,28 @@ (ns lipas.schema.sports-sites - (:require [lipas.data.activities :as activities] - [lipas.data.admins :as admins] + (:require [lipas.data.admins :as admins] [lipas.data.cities :as cities] [lipas.data.owners :as owners] + [lipas.data.activities :as activities] [lipas.data.prop-types :as prop-types] - [lipas.data.sports-sites :as sports-sites] + [lipas.schema.sports-sites.activities :as activities-schema] + [lipas.schema.sports-sites.circumstances :as circumstances-schema] + [lipas.schema.common :as common] [lipas.data.types :as types] [lipas.schema.core :as specs] [lipas.utils :as utils] [malli.core :as m] [malli.util :as mu])) -(def circumstances-schema - [:map - [:storage-capacity {:optional true} :string] - [:roof-trusses-operation-model {:optional true} :string] - [:general-information {:optional true} :string] - [:corner-pieces-count {:optional true} :int] - [:audience-toilets-count {:optional true} :int] - [:bus-park-capacity {:optional true} :int] - [:wifi-capacity-sufficient-for-streaming? {:optional true} :boolean] - [:first-aid-comment {:optional true} :string] - [:fixed-cameras? {:optional true} :boolean] - [:stretcher? {:optional true} :boolean] - [:field-level-loading-doors? {:optional true} :boolean] - [:car-parking-economics-model {:optional true} :string] - [:vip-area? {:optional true} :boolean] - [:separate-referee-locker-room? {:optional true} :boolean] - [:audit-date {:optional true} :string] - [:led-screens-or-surfaces-for-ads? {:optional true} :boolean] - [:saunas-count {:optional true} :int] - [:camera-stands? {:optional true} :boolean] - [:wired-microfone-quantity {:optional true} :int] - [:locker-rooms-count {:optional true} :int] - [:car-parking-capacity {:optional true} :int] - [:broadcast-car-park? {:optional true} :boolean] - [:press-conference-space? {:optional true} :boolean] - [:open-floor-space-length-m {:optional true} number?] - [:wifi-available? {:optional true} :boolean] - [:goal-shrinking-elements-count {:optional true} :int] - [:scoreboard-count {:optional true} :int] - [:restaurateur-contact-info {:optional true} :string] - [:vip-area-comment {:optional true} :string] - [:cafe-or-restaurant-has-exclusive-rights-for-products? - {:optional true} - :boolean] - [:gym? {:optional true} :boolean] - [:ticket-sales-operator {:optional true} :string] - [:side-training-space? {:optional true} :boolean] - [:locker-room-quality-comment {:optional true} :string] - [:roof-trusses? {:optional true} :boolean] - [:detached-chair-quantity {:optional true} :int] - [:conference-space-total-capacity-person {:optional true} :int] - [:iff-certification-stickers-in-goals? {:optional true} :boolean] - [:electrical-plan-available? {:optional true} :boolean] - [:audio-mixer-available? {:optional true} :boolean] - [:iff-certified-rink? {:optional true} :boolean] - [:teams-using {:optional true} :string] - [:loading-equipment-available? {:optional true} :boolean] - [:doping-test-facilities? {:optional true} :boolean] - [:wireless-microfone-quantity {:optional true} :int] - [:defibrillator? {:optional true} :boolean] - [:open-floor-space-width-m {:optional true} number?] - [:cafeteria-and-restaurant-capacity-person {:optional true} :int] - [:speakers-aligned-towards-stands? {:optional true} :boolean] - [:conference-space-quantity {:optional true} :int] - [:three-phase-electric-power? {:optional true} :boolean] - [:roof-trusses-capacity-kg {:optional true} :int] - [:open-floor-space-area-m2 {:optional true} number?] - [:detached-tables-quantity {:optional true} :int] - [:available-goals-count {:optional true} :int] - [:player-entrance {:optional true} :string]]) - (def city-code (into [:enum] (sort (keys cities/by-city-code)))) (def city-codes [:set city-code]) (def type-codes [:set (into [:enum] (sort (keys types/all)))]) -;; https://github.com/metosin/malli/issues/670 -(def number-schema number?) - -(defn make-location-schema [geometry-schema] +(defn make-location-schema [feature-schema] [:map [:city [:map - [:city-code city-code] + [:city-code #'city-code] [:neighborhood {:optional true} [:string {:min 1 :max 100}]]]] [:address [:string {:min 1 :max 200}]] @@ -95,41 +33,27 @@ [:map [:type [:enum "FeatureCollection"]] [:features - [:vector - [:map - [:type [:enum "Feature"]] - [:id {:optional true} :string] - [:geometry geometry-schema]]]]]]]) + [:vector feature-schema]]]]]) -(def point-geometry +(def line-string-feature-props [:map - {:title "Point"} - [:type [:enum "Point"]] - [:coordinates [:vector {:min 2 :max 3} number-schema]]]) + [:name {:optional true} :string] + [:lipas-id {:optional true} :int] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]) -(def line-string-geometry - [:map - {:title "LineString"} - [:type [:enum "LineString"]] - [:coordinates [:vector [:vector {:min 2 :max 3} number-schema]]] - [:properties {:optional true} - [:map - [:name {:optional true} :string] - [:lipas-id {:optional true} :int] - [:type-code {:optional true} :int] - [:route-part-difficulty {:optional true} :string] - [:travel-direction {:optional true} :string]]]]) +(def line-string-feature + (mu/assoc common/line-string-feature :properties line-string-feature-props)) +(def point-location (make-location-schema common/point-feature)) +(def line-string-location (make-location-schema line-string-feature)) +(def polygon-location (make-location-schema common/polygon-feature)) -(def polygon-geometry - [:map - {:title "Polygon"} - [:type [:enum "Polygon"]] - [:coordinates [:vector [:vector [:vector {:min 2 :max 3} number-schema]]]]]) - -(def point-location (make-location-schema point-geometry)) -(def line-string-location (make-location-schema line-string-geometry)) -(def polygon-location (make-location-schema polygon-geometry)) +(def owner (into [:enum] (keys owners/all))) +(def owners [:set owner]) +(def admin (into [:enum] (keys admins/all))) +(def admins [:set admin]) (def sports-site-base [:map @@ -137,7 +61,7 @@ ;; Because this is used from :and, both branches need to be open for Malli to work. :closed false} [:lipas-id [:int]] - [:status (into [:enum] (keys sports-sites/statuses))] + [:status #'common/status] [:name [:string {:min 2 :max 100}]] [:marketing-name {:optional true} [:string {:min 2 :max 100}]] @@ -147,8 +71,8 @@ [:string {:min 2 :max 100}]] [:en {:optional true} [:string {:min 2 :max 100}]]]] - [:owner (into [:enum] (keys owners/all))] - [:admin (into [:enum] (keys admins/all))] + [:owner #'owner] + [:admin #'admin] [:email {:optional true} [:re specs/email-regex]] [:www {:optional true} @@ -186,49 +110,50 @@ (def sports-site (into [:multi {:title "SportsSite" :dispatch (fn [x] - (-> x :type :type-code))} - [:malli.core/default :any]] + (-> x :type :type-code))}] (for [[type-code {:keys [geometry-type props] :as x}] (sort-by key types/all) :let [activity (get activities/by-type-code type-code) activity-key (some-> activity :value keyword) floorball? (= 2240 type-code)]] [type-code (make-sports-site-schema - {:title (str type-code " - " (:fi (:name x))) - :type-codes #{type-code} - :location-schema (case geometry-type - "Point" #'point-location - "LineString" #'line-string-location - "Polygon" #'polygon-location) - :extras-schema (cond-> [:map] - (seq props) - (conj [:properties - {:optional true} - (into [:map] - (for [[k schema] (select-keys prop-types/schemas (keys props))] - [k {:optional true} schema]))]) - - floorball? - (conj [:circumstances - {:optional true - :description "Floorball information"} - circumstances-schema]) - - activity - (conj [:activities - {:optional true} - [:map - [activity-key - {:optional true} - (case activity-key - :outdoor-recreation-areas #'activities/outdoor-recreation-areas-schema - :outdoor-recreation-facilities #'activities/outdoor-recreation-facilities-schema - :outdoor-recreation-routes #'activities/outdoor-recreation-routes-schema - :cycling #'activities/cycling-schema - :paddling #'activities/paddling-schema - :birdwatching #'activities/birdwatching-schema - :fishing #'activities/fishing-schema)]]]))})]))) + {:title (str type-code " - " (:fi (:name x))) + :type-codes #{type-code} + :location-schema (case geometry-type + "Point" #'point-location + "LineString" #'line-string-location + "Polygon" #'polygon-location) + :extras-schema (cond-> [:map] + (seq props) + (conj [:properties + {:optional true} + (into [:map] + (for [[k schema] (select-keys prop-types/schemas (keys props))] + [k {:optional true} schema]))]) + + floorball? + (conj [:circumstances + {:optional true + :description "Floorball information"} + #'circumstances-schema/circumstances]) + + activity + (conj [:activities + {:optional true} + [:map + [activity-key + {:optional true} + (case activity-key + :outdoor-recreation-areas #'activities-schema/outdoor-recreation-areas + :outdoor-recreation-facilities #'activities-schema/outdoor-recreation-facilities + :outdoor-recreation-routes #'activities-schema/outdoor-recreation-routes + :cycling #'activities-schema/cycling + :paddling #'activities-schema/paddling + :birdwatching #'activities-schema/birdwatching + :fishing #'activities-schema/fishing)]]]))})]))) (comment + (mu/get sports-site 101) + (m/validate [:vector sports-site] [{:lipas-id 1 :status "active" diff --git a/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc new file mode 100644 index 000000000..24513d2b5 --- /dev/null +++ b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc @@ -0,0 +1,13 @@ +(ns lipas.schema.sports-sites.activities + (:require [lipas.data.activities :as activities-data])) + +(def activity (into [:enum] (keys activities-data/activities))) +(def activities [:set activity]) + +(def fishing activities-data/fishing-schema) +(def outdoor-recreation-areas activities-data/outdoor-recreation-areas-schema) +(def outdoor-recreation-routes activities-data/outdoor-recreation-routes-schema) +(def outdoor-recreation-facilities activities-data/outdoor-recreation-facilities-schema) +(def cycling activities-data/cycling-schema) +(def paddling activities-data/paddling-schema) +(def birdwatching activities-data/birdwatching-schema) diff --git a/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc b/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc new file mode 100644 index 000000000..00b84bddb --- /dev/null +++ b/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc @@ -0,0 +1,63 @@ +(ns lipas.schema.sports-sites.circumstances) + +(def circumstances + [:map {:title "Circumstances" + :description "Floorball related circumstances"} + [:storage-capacity {:optional true} :string] + [:roof-trusses-operation-model {:optional true} :string] + [:general-information {:optional true} :string] + [:corner-pieces-count {:optional true} :int] + [:audience-toilets-count {:optional true} :int] + [:bus-park-capacity {:optional true} :int] + [:wifi-capacity-sufficient-for-streaming? {:optional true} :boolean] + [:first-aid-comment {:optional true} :string] + [:fixed-cameras? {:optional true} :boolean] + [:stretcher? {:optional true} :boolean] + [:field-level-loading-doors? {:optional true} :boolean] + [:car-parking-economics-model {:optional true} :string] + [:vip-area? {:optional true} :boolean] + [:separate-referee-locker-room? {:optional true} :boolean] + [:audit-date {:optional true} :string] + [:led-screens-or-surfaces-for-ads? {:optional true} :boolean] + [:saunas-count {:optional true} :int] + [:camera-stands? {:optional true} :boolean] + [:wired-microfone-quantity {:optional true} :int] + [:locker-rooms-count {:optional true} :int] + [:car-parking-capacity {:optional true} :int] + [:broadcast-car-park? {:optional true} :boolean] + [:press-conference-space? {:optional true} :boolean] + [:open-floor-space-length-m {:optional true} number?] + [:wifi-available? {:optional true} :boolean] + [:goal-shrinking-elements-count {:optional true} :int] + [:scoreboard-count {:optional true} :int] + [:restaurateur-contact-info {:optional true} :string] + [:vip-area-comment {:optional true} :string] + [:cafe-or-restaurant-has-exclusive-rights-for-products? + {:optional true} + :boolean] + [:gym? {:optional true} :boolean] + [:ticket-sales-operator {:optional true} :string] + [:side-training-space? {:optional true} :boolean] + [:locker-room-quality-comment {:optional true} :string] + [:roof-trusses? {:optional true} :boolean] + [:detached-chair-quantity {:optional true} :int] + [:conference-space-total-capacity-person {:optional true} :int] + [:iff-certification-stickers-in-goals? {:optional true} :boolean] + [:electrical-plan-available? {:optional true} :boolean] + [:audio-mixer-available? {:optional true} :boolean] + [:iff-certified-rink? {:optional true} :boolean] + [:teams-using {:optional true} :string] + [:loading-equipment-available? {:optional true} :boolean] + [:doping-test-facilities? {:optional true} :boolean] + [:wireless-microfone-quantity {:optional true} :int] + [:defibrillator? {:optional true} :boolean] + [:open-floor-space-width-m {:optional true} number?] + [:cafeteria-and-restaurant-capacity-person {:optional true} :int] + [:speakers-aligned-towards-stands? {:optional true} :boolean] + [:conference-space-quantity {:optional true} :int] + [:three-phase-electric-power? {:optional true} :boolean] + [:roof-trusses-capacity-kg {:optional true} :int] + [:open-floor-space-area-m2 {:optional true} number?] + [:detached-tables-quantity {:optional true} :int] + [:available-goals-count {:optional true} :int] + [:player-entrance {:optional true} :string]]) diff --git a/webapp/test/clj/lipas/backend/handler_test.clj b/webapp/test/clj/lipas/backend/handler_test.clj index 3b6024bb0..863659fd7 100644 --- a/webapp/test/clj/lipas/backend/handler_test.clj +++ b/webapp/test/clj/lipas/backend/handler_test.clj @@ -6,6 +6,7 @@ [lipas.backend.core :as core] [lipas.backend.jwt :as jwt] [lipas.data.loi :as loi] + [lipas.data.status :as status] [lipas.schema.core] [lipas.seed :as seed] [lipas.test-utils :refer [->transit <-transit <-json ->json app search db] :as tu] @@ -119,7 +120,7 @@ _ (doseq [loi lois-with-every-status] (core/index-loi! search loi :sync)) responses (mapv #(app (-> (mock/request :get (str "/api/lois/status/" %)) (mock/content-type "application/json"))) - (keys loi/statuses)) + (keys status/statuses)) bodies (mapv (comp first <-json :body) responses)] (is (= "planning" (:status (nth bodies 0)))) (is (= "planned" (:status (nth bodies 1)))) From 17c443f433e08dd8576097b4f3803dfa198433e0 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Mon, 23 Dec 2024 18:50:04 +0200 Subject: [PATCH 09/13] Add descriptions to sports-site schemas --- webapp/src/cljc/lipas/data/activities.cljc | 8 ++- .../src/cljc/lipas/schema/sports_sites.cljc | 65 +++++++++++++------ 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/webapp/src/cljc/lipas/data/activities.cljc b/webapp/src/cljc/lipas/data/activities.cljc index d50f9ffb9..e440bb3dd 100644 --- a/webapp/src/cljc/lipas/data/activities.cljc +++ b/webapp/src/cljc/lipas/data/activities.cljc @@ -12,7 +12,13 @@ (defn collect-schema [m] - (into [:map] (map (juxt first (constantly {:optional true}) (comp :schema second)) m))) + (into [:map] (map (juxt first + (fn [m] + {:optional true + :description (get-in (second m) [:field :description :en])}) + (comp :schema second)) + m))) + (def duration-schema [:map [:min {:optional true} common-schema/number] diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index c5b2d9e4a..af3215bac 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -13,10 +13,19 @@ [malli.core :as m] [malli.util :as mu])) -(def city-code (into [:enum] (sort (keys cities/by-city-code)))) +(def city-code + (into [:enum {:title "CityCode" + :description "Official municipality identifier https://stat.fi/fi/luokitukset/kunta/kunta_1_20240101"}] + (sort (keys cities/by-city-code)))) + (def city-codes [:set city-code]) -(def type-codes [:set (into [:enum] (sort (keys types/all)))]) +(def type-code + (into [:enum {:title "TypeCode" + :description "Sports facility type according to LIPAS classification https://www.jyu.fi/fi/liikunta/yhteistyo/lipas-liikunnan-paikkatietojarjestelma"}] + (sort (keys types/all)))) + +(def type-codes [:set type-code]) (defn make-location-schema [feature-schema] [:map @@ -50,10 +59,19 @@ (def line-string-location (make-location-schema line-string-feature)) (def polygon-location (make-location-schema common/polygon-feature)) -(def owner (into [:enum] (keys owners/all))) -(def owners [:set owner]) -(def admin (into [:enum] (keys admins/all))) -(def admins [:set admin]) +(def owner + (into [:enum {:title "Onwer" + :description "Owner entity of the sports facility."}] + (keys owners/all))) + +(def owners [:set #'owner]) + +(def admin + (into [:enum {:title "Admin" + :description "Administrative entity of the sports facility."}] + (keys admins/all))) + +(def admins [:set #'admin]) (def sports-site-base [:map @@ -62,30 +80,31 @@ :closed false} [:lipas-id [:int]] [:status #'common/status] - [:name [:string {:min 2 :max 100}]] - [:marketing-name {:optional true} + [:name {:description "The official name of the sports facility"} + [:string {:min 2 :max 100}]] + [:marketing-name {:optional true :description "Marketing name or common name of the sports facility."} [:string {:min 2 :max 100}]] - [:name-localized {:optional true} + [:name-localized {:optional true :description "The official name of the sports facility localized."} [:map - [:se {:optional true} + [:se {:optional true :description "Swedish translation of the official name of the sports facility."} [:string {:min 2 :max 100}]] - [:en {:optional true} + [:en {:optional true :description "English translation of the official name of the sports facility."} [:string {:min 2 :max 100}]]]] [:owner #'owner] [:admin #'admin] - [:email {:optional true} + [:email {:optional true :description "Email address of the sports facility."} [:re specs/email-regex]] - [:www {:optional true} + [:www {:optional true :description "Website of the sports facility."} [:string {:min 1 :max 500}]] - [:reservations-link {:optional true} + [:reservations-link {:optional true :description "Link to external booking system."} [:string {:min 1 :max 500}]] - [:phone-number {:optional true} + [:phone-number {:optional true :description "Phone number of the sports facility"} [:string {:min 1 :max 50}]] - [:comment {:optional true} + [:comment {:optional true :description "Additional information."} [:string {:min 1 :max 2048}]] - [:construction-year {:optional true} + [:construction-year {:optional true :description "Year of construction of the sports facility"} [:int {:min 1800 :max (+ 10 utils/this-year)}]] - [:renovation-years {:optional true} + [:renovation-years {:optional true :description "Years of major renovation of the sports facility"} [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]]]) (defn make-sports-site-schema [{:keys [title @@ -116,7 +135,8 @@ activity-key (some-> activity :value keyword) floorball? (= 2240 type-code)]] [type-code (make-sports-site-schema - {:title (str type-code " - " (:fi (:name x))) + {:title (str type-code " - " (:en (:name x))) + :description (get-in x [:description :en]) :type-codes #{type-code} :location-schema (case geometry-type "Point" #'point-location @@ -128,7 +148,9 @@ {:optional true} (into [:map] (for [[k schema] (select-keys prop-types/schemas (keys props))] - [k {:optional true} schema]))]) + [k {:optional true + :description (get-in prop-types/all [k :description :en])} + schema]))]) floorball? (conj [:circumstances @@ -138,7 +160,8 @@ activity (conj [:activities - {:optional true} + {:optional true + :description "Enriched content for Luontoon.fi service."} [:map [activity-key {:optional true} From 2db0507edbaf0baeaee4fd6f2e1762527c44d0f7 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Wed, 25 Dec 2024 14:49:53 +0200 Subject: [PATCH 10/13] Cleanup --- webapp/src/clj/lipas/backend/api/v2.clj | 28 +++++++++---------- webapp/src/cljc/lipas/data/activities.cljc | 6 ++-- webapp/src/cljc/lipas/schema/common.cljc | 6 ++-- webapp/src/cljc/lipas/schema/lois.cljc | 8 +++--- .../src/cljc/lipas/schema/sports_sites.cljc | 22 +++++++++++---- .../lipas/schema/sports_sites/activities.cljc | 5 +++- .../schema/sports_sites/circumstances.cljc | 7 +++-- 7 files changed, 48 insertions(+), 34 deletions(-) diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index e06afc0d4..7c504a3c1 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -129,30 +129,25 @@ [:city-codes {:optional true :decode/string decode-heisenparam - #_#_ - :description (->> cities/by-city-code - (sort-by key) - (map (fn [[code x]] - (str "* " code " - " (:fi (:name x)) ""))) - (str/join "\n") - (str "City-codes:\n"))} + :description (-> sports-sites-schema/city-codes + second + :description)} #'sports-sites-schema/city-codes] [:type-codes {:optional true :decode/string decode-heisenparam - #_#_ - :description (->> types/all - (sort-by key) - (map (fn [[code x]] - (str "* " code " - " (:fi (:name x)) ""))) - (str/join "\n") - (str "Type-codes:\n"))} + :description (-> sports-sites-schema/type-codes + second + :description)} #'sports-sites-schema/type-codes] [:admins {:optional true - :decode/string decode-heisenparam} + :decode/string decode-heisenparam + :description (-> sports-sites-schema/admins + second + :description)} #'sports-sites-schema/admins] [:owners @@ -162,6 +157,9 @@ [:activities {:optional true + :description (-> activities-schema/activities + second + :description) :decode/string decode-heisenparam} #'activities-schema/activities]]} diff --git a/webapp/src/cljc/lipas/data/activities.cljc b/webapp/src/cljc/lipas/data/activities.cljc index e440bb3dd..e50684d78 100644 --- a/webapp/src/cljc/lipas/data/activities.cljc +++ b/webapp/src/cljc/lipas/data/activities.cljc @@ -613,7 +613,7 @@ (mu/dissoc :rules)) [:map [:id [:string]] - [:geometries common-schema/line-string-fcoll] + [:geometries common-schema/line-string-feature-collection] [:accessibility-categorized {:optional true} [:map [:mobility-impaired {:optional true} common-schema/localized-string] @@ -944,7 +944,7 @@ common-route-props-schema [:map [:id [:string]] - [:geometries common-schema/line-string-fcoll] + [:geometries common-schema/line-string-feature-collection] [:route-name {:optional true} common-schema/localized-string] [:cycling-activities {:optional true} [:sequential (into [:enum] (keys cycling-activities))]] @@ -1280,7 +1280,7 @@ common-route-props-schema [:map [:id [:string]] - [:geometries common-schema/line-string-fcoll] + [:geometries common-schema/line-string-feature-collection] [:route-name {:optional true} common-schema/localized-string] [:paddling-activities {:optional true} [:sequential (into [:enum] (keys paddling-activities))]] diff --git a/webapp/src/cljc/lipas/schema/common.cljc b/webapp/src/cljc/lipas/schema/common.cljc index 209b362b8..fd35f3d56 100644 --- a/webapp/src/cljc/lipas/schema/common.cljc +++ b/webapp/src/cljc/lipas/schema/common.cljc @@ -56,20 +56,20 @@ [:geometry #'polygon-geometry] [:properties {:optional true} [:map]]]) -(def point-fcoll +(def point-feature-collection [:map [:type [:enum "FeatureCollection"]] [:features [:sequential point-feature]]]) -(def line-string-fcoll +(def line-string-feature-collection [:map [:type [:enum "FeatureCollection"]] [:features [:sequential #'line-string-feature]]]) -(def polygon-fcoll +(def polygon-feature-collection [:map [:type [:enum "FeatureCollection"]] [:features diff --git a/webapp/src/cljc/lipas/schema/lois.cljc b/webapp/src/cljc/lipas/schema/lois.cljc index 6662e3bfa..c470c9827 100644 --- a/webapp/src/cljc/lipas/schema/lois.cljc +++ b/webapp/src/cljc/lipas/schema/lois.cljc @@ -20,14 +20,14 @@ [(:value type-v) (into [:map {:description (str cat-k " > " (:value type-v)) - :title (-> type-v :label :fi)} + :title (-> type-v :label :en)} [:id [:string]] [:event-date [:string]] #_[:created-at [:string]] [:geometries (case (:geom-type type-v) - ("Polygon") #'common/polygon-fcoll - ("LineString") #'common/line-string-fcoll - #'common/point-fcoll)] + ("Polygon") #'common/polygon-feature-collection + ("LineString") #'common/line-string-feature-collection + #'common/point-feature-collection)] [:status common/status] [:loi-category [:enum cat-k]] [:loi-type [:enum (:value type-v)]]] diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index af3215bac..9a0405483 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -18,14 +18,20 @@ :description "Official municipality identifier https://stat.fi/fi/luokitukset/kunta/kunta_1_20240101"}] (sort (keys cities/by-city-code)))) -(def city-codes [:set city-code]) +(def city-codes + [:set {:title "CityCodes" + :description (-> city-code second :description)} + city-code]) (def type-code (into [:enum {:title "TypeCode" :description "Sports facility type according to LIPAS classification https://www.jyu.fi/fi/liikunta/yhteistyo/lipas-liikunnan-paikkatietojarjestelma"}] (sort (keys types/all)))) -(def type-codes [:set type-code]) +(def type-codes + [:set {:title "TypeCodes" + :description (-> type-code second :description)} + type-code]) (defn make-location-schema [feature-schema] [:map @@ -64,14 +70,20 @@ :description "Owner entity of the sports facility."}] (keys owners/all))) -(def owners [:set #'owner]) +(def owners + [:set {:title "Admins" + :description (-> owner second :description)} + #'owner]) (def admin (into [:enum {:title "Admin" :description "Administrative entity of the sports facility."}] (keys admins/all))) -(def admins [:set #'admin]) +(def admins + [:set {:title "Admins" + :description (-> admin second :description)} + #'admin]) (def sports-site-base [:map @@ -156,7 +168,7 @@ (conj [:circumstances {:optional true :description "Floorball information"} - #'circumstances-schema/circumstances]) + #'circumstances-schema/floorball]) activity (conj [:activities diff --git a/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc index 24513d2b5..4d10ff09e 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc @@ -2,7 +2,10 @@ (:require [lipas.data.activities :as activities-data])) (def activity (into [:enum] (keys activities-data/activities))) -(def activities [:set activity]) +(def activities + [:set {:title "Activities" + :description "Enriched activity related content for Luontoon.fi service. Certain sports facility types may contain data about activities that can be practiced at the facility."} + activity]) (def fishing activities-data/fishing-schema) (def outdoor-recreation-areas activities-data/outdoor-recreation-areas-schema) diff --git a/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc b/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc index 00b84bddb..6091b209f 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites/circumstances.cljc @@ -1,8 +1,9 @@ (ns lipas.schema.sports-sites.circumstances) -(def circumstances - [:map {:title "Circumstances" - :description "Floorball related circumstances"} +;; TODO check fields that are exposed via public API before release +(def floorball + [:map {:title "FloorballCircumstances" + :description "Enriched floorball facility information"} [:storage-capacity {:optional true} :string] [:roof-trusses-operation-model {:optional true} :string] [:general-information {:optional true} :string] From d4080bbeb713374f2ea0c66e02f9624b28138fd7 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Thu, 26 Dec 2024 16:24:43 +0200 Subject: [PATCH 11/13] Add endpoints for types and individual records, documentation --- webapp/src/clj/lipas/backend/api/v2.clj | 373 ++++++++++++------ webapp/src/clj/lipas/backend/core.clj | 15 + webapp/src/cljc/lipas/data/prop_types.cljc | 5 - webapp/src/cljc/lipas/data/types.cljc | 22 ++ webapp/src/cljc/lipas/schema/common.cljc | 2 + webapp/src/cljc/lipas/schema/lois.cljc | 5 +- .../src/cljc/lipas/schema/sports_sites.cljc | 18 +- .../cljc/lipas/schema/sports_sites/types.cljc | 55 +++ webapp/src/cljs/lipas/ui/sports_sites/db.cljs | 2 +- 9 files changed, 349 insertions(+), 148 deletions(-) create mode 100644 webapp/src/cljc/lipas/schema/sports_sites/types.cljc diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index 7c504a3c1..f356697b3 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -5,6 +5,7 @@ [lipas.schema.lois :as lois-schema] [lipas.schema.sports-sites :as sports-sites-schema] [lipas.schema.sports-sites.activities :as activities-schema] + [lipas.schema.sports-sites.types :as types-schema] [reitit.coercion.malli] [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] @@ -32,9 +33,20 @@ "circumstances"]) (defn decode-heisenparam - "Coerces singular and repeating params into a collection. Singular - query param comes in as scalar, while multiple params come in as - vector. Handles also possibly comma-separated vals." + "Normalizes query parameters into a collection by handling different input formats: + + - Single value: Converts a scalar string into a vector by splitting on commas + - Multiple values: Preserves existing vector format + - Empty/nil: Returns the input unchanged + + Examples: + (decode-heisenparam \"a,b,c\") ;=> [\"a\" \"b\" \"c\"] + (decode-heisenparam [\"a\" \"b\"]) ;=> [\"a\" \"b\"] + (decode-heisenparam \"single\") ;=> [\"single\"] + (decode-heisenparam nil) ;=> nil + + Used for normalizing HTTP query parameters where the same parameter name + can appear multiple times or contain comma-separated values." [x] (cond-> x (string? x) (str/split #","))) @@ -77,145 +89,248 @@ {:from (* (dec page) page-size) :size page-size :track_total_hits 50000 - #_#_:_source {:includes sports-site-keys} :query {:bool {:must (cond-> [] statuses (conj {:terms {:status.keyword statuses}}) types (conj {:terms {:loi-type.keyword types}}) categories (conj {:terms {:loi-category.keyword categories}}))}}}) -(defn routes [{:keys [db search] :as ctx}] +(defn routes [{:keys [db search] :as _ctx}] (let [ui-handler (swagger-ui/create-swagger-ui-handler - {:url "/api-v2/openapi.json"})] + {:url "/api-v2/openapi.json"})] ["/api-v2" - {:openapi {:id :api-v2} + {:openapi + {:id :api-v2 + + :info {:title "LIPAS API V2" + :summary "API for Finnish sports and recreational facility database LIPAS" + :description "The LIPAS system provides comprehensive data about sports and recreational facilities in Finland. The API is organized into three main sections: + +**Sports Sites** +The core entities of LIPAS. Each sports facility is classified using a hierarchical type system, where specific facility types belong to subcategories within seven main categories. Each facility type has its own specific set of properties. + +**Sports Site Categories** +Access to the hierarchical type classification system used for categorizing sports facilities. This helps in understanding the structure and relationships between different facility types. + +**Locations of Interest** +Additional non-facility entities in LIPAS, that complement the sports facility data." + + :contact + {:name "Support, feature requests and bug reports" + :url "https://github.com/lipas-liikuntapaikat/lipas/issues" + :email "lipasinfo@jyu.fi"} + + :license + {:name "Creative Commons Attribution 4.0 International" + :identifier "CC-BY-SA-4.0" + :url "https://creativecommons.org/licenses/by-sa/4.0/"}} + + ;; These get merged in a wild way + #_#_:tags [{:name "Sports Sites" + :description "The core entities of LIPAS."} + {:name "Sports Site Categories" + :description "Hierarchical categorization of sports facilities"} + {:name "Locations of Interest" + :description "Additional non-facility entities in LIPAS"}] + + } ;; The regular handle is still using swagger-spec, so :openapi :id doesn't hide ;; these routes from that. - :swagger {:id :hide-from-default} + :swagger {:id :hide-from-default} :coercion reitit.coercion.malli/coercion} + ["/sports-site-categories" + {:tags ["Sports Site Categories"]} + + ["" + {:get + {:summary "Get all sports site categories" + :handler + (fn [_req] + {:status 200 + :body (core/get-categories)}) + + :responses {200 {:body [:sequential #'types-schema/type]}}}}] + + ["/{type-code}" + {:get + {:summary "Get single sports site category by type-code" + :handler + (fn [req] + (let [type-code (-> req :parameters :path :type-code)] + {:status 200 + :body (core/get-category type-code)})) + + :parameters + {:path [:map [:type-code {:description (-> types-schema/type-code + second + :description)} + #'types-schema/type-code]]} + + :responses {200 {:body #'types-schema/type}}}}]] + ["/sports-sites" - {:tags ["sports-sites"] - :get - {:handler - (fn [req] - (tap> (:parameters req)) - (let [params (:query (:parameters req)) - query (->sports-sites-query params) - _ (tap> query) - results (core/search search query)] - (tap> results) - {:status 200 - :body - {:items (-> results :body :hits :hits (->> (keep :_source))) - :pagination (let [total-items (-> results :body :hits :total :value)] - (->pagination (assoc params :total-items total-items)))}})) - - :parameters {:query - [:map - - [:page {:optional true} - [:int {:min 1 :json-schema/default 1}]] - - [:page-size {:optional true} - [:int {:min 1 :max 100 :json-schema/default 10}]] - - [:statuses - {:optional true - :decode/string decode-heisenparam - :json-schema/default #{"active" "out-of-service-temporarily"}} - #'common-schema/statuses] - - [:city-codes - {:optional true - :decode/string decode-heisenparam - :description (-> sports-sites-schema/city-codes - second - :description)} - #'sports-sites-schema/city-codes] - - [:type-codes - {:optional true - :decode/string decode-heisenparam - :description (-> sports-sites-schema/type-codes - second - :description)} - #'sports-sites-schema/type-codes] - - [:admins - {:optional true - :decode/string decode-heisenparam - :description (-> sports-sites-schema/admins - second - :description)} - #'sports-sites-schema/admins] - - [:owners - {:optional true - :decode/string decode-heisenparam} - #'sports-sites-schema/owners] - - [:activities - {:optional true - :description (-> activities-schema/activities - second - :description) - :decode/string decode-heisenparam} - #'activities-schema/activities]]} - - :responses {200 {:body [:map - [:items - [:vector {:title "SportSites"} - #'sports-sites-schema/sports-site]] - [:pagination {:title "Pagination"} - pagination-schema]]}}}}] + {:tags ["Sports Sites"]} + + ["" + {:get + {:summary "Get a paginated list of sports sites" + :handler (fn [req] + (tap> (:parameters req)) + (let [params (:query (:parameters req)) + query (->sports-sites-query params) + _ (tap> query) + results (core/search search query)] + (tap> results) + {:status 200 + :body + {:items (-> results :body :hits :hits (->> (keep :_source))) + :pagination (let [total-items (-> results :body :hits :total :value)] + (->pagination (assoc params :total-items total-items)))}})) + + :parameters {:query + [:map + + [:page {:optional true} + [:int {:min 1 :json-schema/default 1}]] + + [:page-size {:optional true} + [:int {:min 1 :max 100 :json-schema/default 10}]] + + [:statuses + {:optional true + :decode/string decode-heisenparam + :json-schema/default #{"active" "out-of-service-temporarily"}} + #'common-schema/statuses] + + [:city-codes + {:optional true + :decode/string decode-heisenparam + :description (-> sports-sites-schema/city-codes + second + :description)} + #'sports-sites-schema/city-codes] + + [:type-codes + {:optional true + :decode/string decode-heisenparam + :description (-> types-schema/type-codes + second + :description)} + #'types-schema/type-codes] + + [:admins + {:optional true + :decode/string decode-heisenparam + :description (-> sports-sites-schema/admins + second + :description)} + #'sports-sites-schema/admins] + + [:owners + {:optional true + :decode/string decode-heisenparam} + #'sports-sites-schema/owners] + + [:activities + {:optional true + :description (-> activities-schema/activities + second + :description) + :decode/string decode-heisenparam} + #'activities-schema/activities]]} + + :responses {200 {:body [:map + [:items + [:vector {:title "SportSites"} + #'sports-sites-schema/sports-site]] + [:pagination {:title "Pagination"} + pagination-schema]]}}}}] + + ["/{lipas-id}" + {:get + {:summary "Get single sports facility by lipas-id" + + :handler (fn [req] + (tap> (:parameters req)) + (let [lipas-id (-> req :parameters :path :lipas-id)] + {:status 200 + :body (doto (core/get-sports-site db lipas-id) tap>)})) + + :parameters {:path [:map + [:lipas-id + {:description "Lipas-id of the sports facility"} + #'sports-sites-schema/lipas-id]]} + + :responses {200 {:body #'sports-sites-schema/sports-site}}}}]] ["/lois" - {:tags ["locations-of-interest"] - :get {:handler (fn [req] - (tap> (:parameters req)) - (let [params (:query (:parameters req)) - query (->lois-query params) - _ (tap> query) - results (core/search-lois* search query)] - (tap> results) - {:status 200 - :body - {:items (-> results :body :hits :hits (->> (keep :_source))) - :pagination (let [total-items (-> results :body :hits :total :value)] - (->pagination (assoc params :total-items total-items)))}})) - - :parameters {:query - [:map - - [:page {:optional true} - [:int {:min 1 :json-schema/default 1}]] - - [:page-size {:optional true} - [:int {:min 1 :max 100 :json-schema/default 10}]] - - [:statuses - {:optional true - :decode/string decode-heisenparam - :json-schema/default #{"active" "out-of-service-temporarily"}} - #'common-schema/statuses] - - [:categories - {:optional true - :decode/string decode-heisenparam} - #'lois-schema/loi-categories] - - [:types - {:optional true - :decode/string decode-heisenparam} - #'lois-schema/loi-types]]} - - :responses {200 {:body [:map - [:items - [:vector {:title "LocationsOfInterest"} - #'lois-schema/loi]] - [:pagination {:title "Pagination"} - pagination-schema]]}}}}] + {:tags ["Locations of Interest"] + } + + ["" + {:get {:summary "Get a paginated list of locations of interest" + :handler (fn [req] + (tap> (:parameters req)) + (let [params (:query (:parameters req)) + query (->lois-query params) + _ (tap> query) + results (core/search-lois* search query)] + (tap> results) + {:status 200 + :body + {:items (-> results :body :hits :hits (->> (keep :_source))) + :pagination (let [total-items (-> results :body :hits :total :value)] + (->pagination (assoc params :total-items total-items)))}})) + + :parameters {:query + [:map + + [:page {:optional true} + [:int {:min 1 :json-schema/default 1}]] + + [:page-size {:optional true} + [:int {:min 1 :max 100 :json-schema/default 10}]] + + [:statuses + {:optional true + :decode/string decode-heisenparam + :json-schema/default #{"active" "out-of-service-temporarily"}} + #'common-schema/statuses] + + [:categories + {:optional true + :decode/string decode-heisenparam} + #'lois-schema/loi-categories] + + [:types + {:optional true + :decode/string decode-heisenparam} + #'lois-schema/loi-types]]} + + :responses {200 {:body [:map + [:items + [:vector {:title "LocationsOfInterest"} + #'lois-schema/loi]] + [:pagination {:title "Pagination"} + pagination-schema]]}}}}] + + ["/{loi-id}" + {:get + {:summary "Get single Location of Interest by id" + + :handler (fn [req] + (tap> (:parameters req)) + (let [loi-id (-> req :parameters :path :loi-id)] + {:status 200 + :body (doto (core/get-loi search loi-id) tap>)})) + + :parameters {:path [:map + [:loi-id + {:description "UUID v4 of the Location if Interest"} + #'lois-schema/loi-id]]} + + :responses {200 {:body #'lois-schema/loi}}}}]] ["/openapi.json" {:get @@ -224,8 +339,8 @@ :handler (openapi/create-openapi-handler)}}] ["/swagger-ui" - {:get {:no-doc true + {:get {:no-doc true :handler ui-handler}}] ["/swagger-ui/*" - {:get {:no-doc true + {:get {:no-doc true :handler ui-handler}}]])) diff --git a/webapp/src/clj/lipas/backend/core.clj b/webapp/src/clj/lipas/backend/core.clj index 63e38fb8e..5617ec37b 100644 --- a/webapp/src/clj/lipas/backend/core.clj +++ b/webapp/src/clj/lipas/backend/core.clj @@ -799,6 +799,8 @@ (log/error ex) (db/update-elevation-status! db lipas-id "failed")))))) +;;; Newsletter ;;; + (defn get-newsletter [config] (newsletter/retrieve config)) @@ -843,6 +845,8 @@ :credentials-provider credentials-provider}))) +;;; LOI ;;; + (defn ->lois-es-query [{:keys [location loi-statuses]}] (let [lon (:lon location) @@ -932,7 +936,18 @@ [{:keys [_filename _data _user] :as params}] (utp-cms/upload-image! params)) +;;; Types ;;; + +(defn get-categories + [] + (map types/->type (vals types/active))) + +(defn get-category + [type-code] + (types/->type (types/active type-code))) + (comment + (get-categories) (require '[lipas.backend.config :as config]) (def db-spec (:db config/default-config)) (def admin (get-user db-spec "admin@lipas.fi")) diff --git a/webapp/src/cljc/lipas/data/prop_types.cljc b/webapp/src/cljc/lipas/data/prop_types.cljc index ecbb89004..51f311a2b 100644 --- a/webapp/src/cljc/lipas/data/prop_types.cljc +++ b/webapp/src/cljc/lipas/data/prop_types.cljc @@ -2,7 +2,6 @@ "Type codes went through a major overhaul in the summer of 2024. This namespace represents the changes made." (:require - [lipas.data.types :as types] [lipas.data.materials :as materials])) (def all @@ -1817,10 +1816,6 @@ :se "Löpbanans, rundbanans el.dyl. längd i meter", :en "The length of the running track, cycling track, etc., in meters"}}}) -(def used - (let [used (set (mapcat (comp keys :props second) types/all))] - (select-keys all used))) - (def schemas (into {} (for [[k m] all] [k (case (:data-type m) diff --git a/webapp/src/cljc/lipas/data/types.cljc b/webapp/src/cljc/lipas/data/types.cljc index cc4f76f95..7da780f96 100644 --- a/webapp/src/cljc/lipas/data/types.cljc +++ b/webapp/src/cljc/lipas/data/types.cljc @@ -3,6 +3,7 @@ (:require [lipas.utils :as utils] [lipas.data.types-old :as old] + [lipas.data.prop-types :as prop-types] #_[lipas.data.types-new :as new] )) @@ -32,3 +33,24 @@ (def sub-category-by-fi-name (utils/index-by (comp :fi :name) (vals sub-categories))) + +(defn ->type + [m] + (-> m + (update :main-category (fn [id] (-> id + main-categories + (dissoc :ptv)))) + (update :sub-category (fn [id] (-> id + sub-categories + (dissoc :ptv :main-category)))) + + (update :props (fn [m] + (->> m + (reduce-kv (fn [res k v] + (conj res (merge v {:key k} (prop-types/all k)))) + []) + (sort-by :priority utils/reverse-cmp)))))) + +(def used-prop-types + (let [used (set (mapcat (comp keys :props second) all))] + (select-keys prop-types/all used))) diff --git a/webapp/src/cljc/lipas/schema/common.cljc b/webapp/src/cljc/lipas/schema/common.cljc index fd35f3d56..99b9a1bbf 100644 --- a/webapp/src/cljc/lipas/schema/common.cljc +++ b/webapp/src/cljc/lipas/schema/common.cljc @@ -6,6 +6,8 @@ ;; https://github.com/metosin/malli/issues/670 (def number number?) (def percentage [number? {:min 0 :max 100}]) +(def -uuid-pattern #"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") +(def uuid [:re {:description "UUID v4 string"} -uuid-pattern]) (def localized-string [:map diff --git a/webapp/src/cljc/lipas/schema/lois.cljc b/webapp/src/cljc/lipas/schema/lois.cljc index c470c9827..a3305e56d 100644 --- a/webapp/src/cljc/lipas/schema/lois.cljc +++ b/webapp/src/cljc/lipas/schema/lois.cljc @@ -5,11 +5,14 @@ [lipas.schema.common :as common] [malli.json-schema :as json-schema])) +(def loi-id common/uuid) (def loi-category (into [:enum] (keys loi/categories))) (def loi-categories [:set loi-category]) + (def loi-type (into [:enum] (for [[_ category] loi/categories [_ type] (:types category)] (:value type)))) + (def loi-types [:set loi-type]) (def loi @@ -21,7 +24,7 @@ (into [:map {:description (str cat-k " > " (:value type-v)) :title (-> type-v :label :en)} - [:id [:string]] + [:id [:string ]] [:event-date [:string]] #_[:created-at [:string]] [:geometries (case (:geom-type type-v) diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index 9a0405483..48a6c4c27 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -6,6 +6,7 @@ [lipas.data.prop-types :as prop-types] [lipas.schema.sports-sites.activities :as activities-schema] [lipas.schema.sports-sites.circumstances :as circumstances-schema] + [lipas.schema.sports-sites.types :as types-schema] [lipas.schema.common :as common] [lipas.data.types :as types] [lipas.schema.core :as specs] @@ -13,6 +14,9 @@ [malli.core :as m] [malli.util :as mu])) +(def lipas-id + [:int {:min 0 :label "Lipas-id" :description "Unique identifier of sports facility in LIPAS system."}]) + (def city-code (into [:enum {:title "CityCode" :description "Official municipality identifier https://stat.fi/fi/luokitukset/kunta/kunta_1_20240101"}] @@ -23,16 +27,6 @@ :description (-> city-code second :description)} city-code]) -(def type-code - (into [:enum {:title "TypeCode" - :description "Sports facility type according to LIPAS classification https://www.jyu.fi/fi/liikunta/yhteistyo/lipas-liikunnan-paikkatietojarjestelma"}] - (sort (keys types/all)))) - -(def type-codes - [:set {:title "TypeCodes" - :description (-> type-code second :description)} - type-code]) - (defn make-location-schema [feature-schema] [:map [:city @@ -53,7 +47,7 @@ (def line-string-feature-props [:map [:name {:optional true} :string] - [:lipas-id {:optional true} :int] + [:lipas-id {:optional true} #'lipas-id] [:type-code {:optional true} :int] [:route-part-difficulty {:optional true} :string] [:travel-direction {:optional true} :string]]) @@ -90,7 +84,7 @@ {:title "Shared Properties" ;; Because this is used from :and, both branches need to be open for Malli to work. :closed false} - [:lipas-id [:int]] + [:lipas-id #'lipas-id] [:status #'common/status] [:name {:description "The official name of the sports facility"} [:string {:min 2 :max 100}]] diff --git a/webapp/src/cljc/lipas/schema/sports_sites/types.cljc b/webapp/src/cljc/lipas/schema/sports_sites/types.cljc new file mode 100644 index 000000000..be82fcf6d --- /dev/null +++ b/webapp/src/cljc/lipas/schema/sports_sites/types.cljc @@ -0,0 +1,55 @@ +(ns lipas.schema.sports-sites.types + (:require [lipas.data.prop-types :as prop-types] + [lipas.data.types :as types] + [lipas.schema.common :as common])) + +(def tags [:sequential [:string {:min 2 :max 100}]]) + +(def type-code + (into [:enum {:title "TypeCode" + :description "Sports facility type according to LIPAS classification https://www.jyu.fi/fi/file/lipas-tyyppikoodit-2024"}] + (keys types/active))) + +(def type-codes + [:set {:title "TypeCodes" + :description (-> type-code second :description)} + #'type-code]) + +(def main-category (into [:enum] (keys types/main-categories))) +(def sub-category (into [:enum] (keys types/sub-categories))) +(def prop-type-key (into [:enum] (keys prop-types/all))) + +(def type + [:map + [:name #'common/localized-string] + [:description #'common/localized-string] + [:tags {:optional true} + [:map + [:fi {:optional true} #'tags] + [:se {:optional true} #'tags] + [:en {:optional true} #'tags]]] + [:type-code #'type-code] + [:main-category + [:map + [:type-code #'main-category] + [:name #'common/localized-string]]] + [:sub-category + [:map + [:type-code #'sub-category] + [:name #'common/localized-string]]] + [:geometry-type [:enum "Point" "LineString" "Polygon"]] + [:props + [:vector + [:map + [:priority [:int]] + [:key #'prop-type-key] + [:name #'common/localized-string] + [:description #'common/localized-string] + [:data-type [:enum "numeric" "boolean" "enum" "enum-coll" "string"]] + [:opts {:optional true} + [:map-of [:string {:min 2 :max 200}] #'common/localized-string]]]]]]) + +(comment + (require '[malli.core :as m]) + (m/schema type) + ) diff --git a/webapp/src/cljs/lipas/ui/sports_sites/db.cljs b/webapp/src/cljs/lipas/ui/sports_sites/db.cljs index 5f3d77859..e119e0620 100644 --- a/webapp/src/cljs/lipas/ui/sports_sites/db.cljs +++ b/webapp/src/cljs/lipas/ui/sports_sites/db.cljs @@ -23,7 +23,7 @@ :types types/all :active-types types/active - :prop-types prop-types/used + :prop-types types/used-prop-types :materials materials/all :building-materials materials/building-materials From 763b3dfd4313c73f16950805de2ead64244ab726 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Thu, 26 Dec 2024 22:02:49 +0200 Subject: [PATCH 12/13] Improve documentation --- webapp/src/cljc/lipas/schema/common.cljc | 28 ++--- .../src/cljc/lipas/schema/sports_sites.cljc | 119 ++++++++++-------- .../lipas/schema/sports_sites/location.cljc | 21 ++++ .../cljc/lipas/schema/sports_sites/types.cljc | 2 +- 4 files changed, 102 insertions(+), 68 deletions(-) create mode 100644 webapp/src/cljc/lipas/schema/sports_sites/location.cljc diff --git a/webapp/src/cljc/lipas/schema/common.cljc b/webapp/src/cljc/lipas/schema/common.cljc index 99b9a1bbf..222d5e803 100644 --- a/webapp/src/cljc/lipas/schema/common.cljc +++ b/webapp/src/cljc/lipas/schema/common.cljc @@ -11,68 +11,64 @@ (def localized-string [:map - [:fi {:optional true} [:string]] - [:se {:optional true} [:string]] - [:en {:optional true} [:string]]]) + [:fi {:optional true :description "Finnish translation"} [:string]] + [:se {:optional true :description "Swedish translation"} [:string]] + [:en {:optional true :description "English translation"} [:string]]]) (def coordinates [:vector {:min 2 :max 3 - :title "Coordinates" :description "WGS84 Lon, Lat and optional altitude in meters"} number?]) (def point-geometry - [:map - {:title "Point"} + [:map {:description "GeoJSON Point geometry"} [:type [:enum "Point"]] [:coordinates #'coordinates]]) (def line-string-geometry - [:map - {:title "LineString"} + [:map {:description "GeoJSON LineString geometry"} [:type [:enum "LineString"]] [:coordinates [:vector #'coordinates]]]) (def polygon-geometry - [:map - {:title "Polygon"} + [:map {:description "GeoJSON Polygon geometry"} [:type [:enum "Polygon"]] [:coordinates [:vector [:vector #'coordinates]]]]) (def point-feature - [:map {:title "PointFeature"} + [:map {:description "GeoJSON Feature with required Point geometry."} [:type [:enum "Feature"]] [:geometry #'point-geometry] [:properties {:optional true} [:map]]]) (def line-string-feature - [:map {:title "LineStringFeature"} + [:map {:description "GeoJSON Feature with required LineString geometry."} [:type [:enum "Feature"]] [:geometry #'line-string-geometry] [:properties {:optional true} [:map]]]) (def polygon-feature - [:map {:title "PolygonFeature"} + [:map {:description "GeoJSON Feature with required Polygon geometry."} [:type [:enum "Feature"]] [:geometry #'polygon-geometry] [:properties {:optional true} [:map]]]) (def point-feature-collection - [:map + [:map {:description "GeoJSON FeatureCollection with required Point geometries."} [:type [:enum "FeatureCollection"]] [:features [:sequential point-feature]]]) (def line-string-feature-collection - [:map + [:map {:description "GeoJSON FeatureCollection with required LineString geometries."} [:type [:enum "FeatureCollection"]] [:features [:sequential #'line-string-feature]]]) (def polygon-feature-collection - [:map + [:map {:description "GeoJSON FeatureCollection with required Polygon geometries."} [:type [:enum "FeatureCollection"]] [:features [:sequential diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index 48a6c4c27..c901f1516 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -6,7 +6,7 @@ [lipas.data.prop-types :as prop-types] [lipas.schema.sports-sites.activities :as activities-schema] [lipas.schema.sports-sites.circumstances :as circumstances-schema] - [lipas.schema.sports-sites.types :as types-schema] + [lipas.schema.sports-sites.location :as location-schema] [lipas.schema.common :as common] [lipas.data.types :as types] [lipas.schema.core :as specs] @@ -28,16 +28,14 @@ city-code]) (defn make-location-schema [feature-schema] - [:map + [:map {:description "Location of the sports facility."} [:city [:map [:city-code #'city-code] - [:neighborhood {:optional true} - [:string {:min 1 :max 100}]]]] - [:address [:string {:min 1 :max 200}]] - [:postal-code [:re specs/postal-code-regex]] - [:postal-office {:optional true} - [:string {:min 1 :max 100}]] + [:neighborhood {:optional true} #'location-schema/neighborhood]]] + [:address #'location-schema/address] + [:postal-code #'location-schema/postal-code] + [:postal-office {:optional true} #'location-schema/postal-code] [:geometries [:map [:type [:enum "FeatureCollection"]] @@ -79,39 +77,46 @@ :description (-> admin second :description)} #'admin]) -(def sports-site-base - [:map - {:title "Shared Properties" - ;; Because this is used from :and, both branches need to be open for Malli to work. - :closed false} - [:lipas-id #'lipas-id] - [:status #'common/status] - [:name {:description "The official name of the sports facility"} - [:string {:min 2 :max 100}]] - [:marketing-name {:optional true :description "Marketing name or common name of the sports facility."} - [:string {:min 2 :max 100}]] - [:name-localized {:optional true :description "The official name of the sports facility localized."} - [:map +(def name [:string {:description "The official name of the sports facility" + :min 2 + :max 100}]) + +(def marketing-name [:string {:min 2 + :max 100 + :description "Marketing name or common name of the sports facility."}]) + +(def name-localized + [:map {:description "The official name of the sports facility localized."} [:se {:optional true :description "Swedish translation of the official name of the sports facility."} [:string {:min 2 :max 100}]] [:en {:optional true :description "English translation of the official name of the sports facility."} - [:string {:min 2 :max 100}]]]] - [:owner #'owner] - [:admin #'admin] - [:email {:optional true :description "Email address of the sports facility."} - [:re specs/email-regex]] - [:www {:optional true :description "Website of the sports facility."} - [:string {:min 1 :max 500}]] - [:reservations-link {:optional true :description "Link to external booking system."} - [:string {:min 1 :max 500}]] - [:phone-number {:optional true :description "Phone number of the sports facility"} - [:string {:min 1 :max 50}]] - [:comment {:optional true :description "Additional information."} - [:string {:min 1 :max 2048}]] - [:construction-year {:optional true :description "Year of construction of the sports facility"} - [:int {:min 1800 :max (+ 10 utils/this-year)}]] - [:renovation-years {:optional true :description "Years of major renovation of the sports facility"} - [:sequential [:int {:min 1800 :max (+ 10 utils/this-year)}]]]]) + [:string {:min 2 :max 100}]]]) + +(def email [:re {:description "Email address of the sports facility."} + specs/email-regex]) + +(def www [:string {:description "Website of the sports facility." + :min 1 + :max 500}]) + +(def reservation-link [:string {:description "Link to external booking system." + :min 1 + :max 500}]) + +(def phone-number [:string {:description "Phone number of the sports facility" + :min 1 + :max 50}]) + +(def comment [:string {:description "Additional information." + :min 1 + :max 2048}]) + +(def construction-year [:int {:description "Year of construction of the sports facility" + :min 1800 + :max (+ 10 utils/this-year)}]) + +(def renovation-years [:sequential {:description "Years of major renovation of the sports facility"} + [:int {:min 1800 :max (+ 10 utils/this-year)}]]) (defn make-sports-site-schema [{:keys [title type-codes @@ -119,21 +124,33 @@ extras-schema location-schema]}] ;; TODO audit - [:and - #'sports-site-base - (mu/merge + (mu/merge + [:map + {:title title + :description description + :closed false} + [:lipas-id #'lipas-id] + [:status #'common/status] + [:name #'name] + [:marketing-name {:optional true} #'marketing-name] + [:name-localized {:optional true} #'name-localized] + [:owner #'owner] + [:admin #'admin] + [:email {:optional true} #'email] + [:www {:optional true} #'www] + [:reservations-link {:optional true} #'reservation-link] + [:phone-number {:optional true} #'phone-number] + [:comment {:optional true} #'comment] + [:construction-year {:optional true} #'construction-year] + [:renovation-years {:optional true} #'renovation-years] + [:type [:map - {:title title - :description description - :closed false} - [:type - [:map - [:type-code (into [:enum] type-codes)]]] - [:location location-schema]] - extras-schema)]) + [:type-code (into [:enum] type-codes)]]] + [:location location-schema]] + extras-schema)) (def sports-site - (into [:multi {:title "SportsSite" + (into [:multi {:description "The core entity of LIPAS. Properties, geometry type and additional attributes vary based on the type of the sports facility." :dispatch (fn [x] (-> x :type :type-code))}] (for [[type-code {:keys [geometry-type props] :as x}] (sort-by key types/all) @@ -180,7 +197,7 @@ :birdwatching #'activities-schema/birdwatching :fishing #'activities-schema/fishing)]]]))})]))) -(comment +#_(comment (mu/get sports-site 101) (m/validate [:vector sports-site] diff --git a/webapp/src/cljc/lipas/schema/sports_sites/location.cljc b/webapp/src/cljc/lipas/schema/sports_sites/location.cljc new file mode 100644 index 000000000..bf25fce44 --- /dev/null +++ b/webapp/src/cljc/lipas/schema/sports_sites/location.cljc @@ -0,0 +1,21 @@ +(ns lipas.schema.sports-sites.location + (:require [lipas.schema.core :as specs])) + +(def address + [:string {:description "Street address of the sports facility." + :min 1 + :max 200}]) + +(def postal-code + [:re {:description "Postal code of the address of the sports facility."} + specs/postal-code-regex]) + +(def postal-office + [:string {:description "Postal office of the address of the sports facility." + :min 1 + :max 100}]) + +(def neighborhood + [:string {:description "Neighborhood or common name for the area of the location." + :min 1 + :max 100}]) diff --git a/webapp/src/cljc/lipas/schema/sports_sites/types.cljc b/webapp/src/cljc/lipas/schema/sports_sites/types.cljc index be82fcf6d..0148f12d0 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites/types.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites/types.cljc @@ -20,7 +20,7 @@ (def prop-type-key (into [:enum] (keys prop-types/all))) (def type - [:map + [:map {:description "Metadata definition for a specific sports facility type in LIPAS"} [:name #'common/localized-string] [:description #'common/localized-string] [:tags {:optional true} From 30f783121468649e5f62914f35063729c73fc058 Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Fri, 27 Dec 2024 17:48:07 +0200 Subject: [PATCH 13/13] More documentation improvements --- webapp/src/clj/lipas/backend/api/v2.clj | 17 +++--- webapp/src/cljc/lipas/data/activities.cljc | 3 +- webapp/src/cljc/lipas/schema/common.cljc | 5 ++ webapp/src/cljc/lipas/schema/lois.cljc | 11 ++-- .../src/cljc/lipas/schema/sports_sites.cljc | 49 ++-------------- .../lipas/schema/sports_sites/activities.cljc | 48 +++++++++++++--- .../lipas/schema/sports_sites/location.cljc | 48 +++++++++++++++- webapp/test/clj/lipas/timestamps_test.clj | 57 +++++++++++++++++++ 8 files changed, 172 insertions(+), 66 deletions(-) create mode 100644 webapp/test/clj/lipas/timestamps_test.clj diff --git a/webapp/src/clj/lipas/backend/api/v2.clj b/webapp/src/clj/lipas/backend/api/v2.clj index f356697b3..3b4c6ed96 100644 --- a/webapp/src/clj/lipas/backend/api/v2.clj +++ b/webapp/src/clj/lipas/backend/api/v2.clj @@ -6,6 +6,7 @@ [lipas.schema.sports-sites :as sports-sites-schema] [lipas.schema.sports-sites.activities :as activities-schema] [lipas.schema.sports-sites.types :as types-schema] + [lipas.schema.sports-sites.location :as location-schema] [reitit.coercion.malli] [reitit.openapi :as openapi] [reitit.swagger-ui :as swagger-ui] @@ -107,10 +108,10 @@ :description "The LIPAS system provides comprehensive data about sports and recreational facilities in Finland. The API is organized into three main sections: **Sports Sites** -The core entities of LIPAS. Each sports facility is classified using a hierarchical type system, where specific facility types belong to subcategories within seven main categories. Each facility type has its own specific set of properties. +The core entities of LIPAS. Each sports facility is classified using a hierarchical type system, where specific facility types belong to subcategories within seven main categories. Each facility type has its own specific set of properties and a defined geometry type (Point, LineString, or Polygon) that describes its spatial representation. **Sports Site Categories** -Access to the hierarchical type classification system used for categorizing sports facilities. This helps in understanding the structure and relationships between different facility types. +Access to the hierarchical type classification system used for categorizing sports facilities. **Locations of Interest** Additional non-facility entities in LIPAS, that complement the sports facility data." @@ -154,7 +155,7 @@ Additional non-facility entities in LIPAS, that complement the sports facility d ["/{type-code}" {:get - {:summary "Get single sports site category by type-code" + {:summary "Get sports site category by type code" :handler (fn [req] (let [type-code (-> req :parameters :path :type-code)] @@ -206,10 +207,10 @@ Additional non-facility entities in LIPAS, that complement the sports facility d [:city-codes {:optional true :decode/string decode-heisenparam - :description (-> sports-sites-schema/city-codes + :description (-> location-schema/city-codes second :description)} - #'sports-sites-schema/city-codes] + #'location-schema/city-codes] [:type-codes {:optional true @@ -249,7 +250,7 @@ Additional non-facility entities in LIPAS, that complement the sports facility d ["/{lipas-id}" {:get - {:summary "Get single sports facility by lipas-id" + {:summary "Get sports facility by lipas-id" :handler (fn [req] (tap> (:parameters req)) @@ -269,7 +270,7 @@ Additional non-facility entities in LIPAS, that complement the sports facility d } ["" - {:get {:summary "Get a paginated list of locations of interest" + {:get {:summary "Get a paginated list of Locations of Interest" :handler (fn [req] (tap> (:parameters req)) (let [params (:query (:parameters req)) @@ -317,7 +318,7 @@ Additional non-facility entities in LIPAS, that complement the sports facility d ["/{loi-id}" {:get - {:summary "Get single Location of Interest by id" + {:summary "Get Location of Interest by id" :handler (fn [req] (tap> (:parameters req)) diff --git a/webapp/src/cljc/lipas/data/activities.cljc b/webapp/src/cljc/lipas/data/activities.cljc index e50684d78..2a0565025 100644 --- a/webapp/src/cljc/lipas/data/activities.cljc +++ b/webapp/src/cljc/lipas/data/activities.cljc @@ -2,8 +2,9 @@ (:require #?(:clj [cheshire.core :as json]) #?(:clj [clojure.data.csv :as csv]) - #?(:clj [clojure.string :as str]) + [clojure.string :as str] [lipas.data.materials :as materials] + [lipas.data.types :as types] [lipas.schema.common :as common-schema] [lipas.utils :as utils] [malli.core :as m] diff --git a/webapp/src/cljc/lipas/schema/common.cljc b/webapp/src/cljc/lipas/schema/common.cljc index 222d5e803..4c38c402d 100644 --- a/webapp/src/cljc/lipas/schema/common.cljc +++ b/webapp/src/cljc/lipas/schema/common.cljc @@ -1,6 +1,11 @@ (ns lipas.schema.common (:require [lipas.data.status :as status])) +(def -iso8601-pattern #"^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?Z$") + +(def iso8601-timestamp [:re {:description "ISO 8601 timestamp in UTC timezone"} + -iso8601-pattern]) + (def status (into [:enum] (keys status/statuses))) (def statuses [:set status]) ;; https://github.com/metosin/malli/issues/670 diff --git a/webapp/src/cljc/lipas/schema/lois.cljc b/webapp/src/cljc/lipas/schema/lois.cljc index a3305e56d..ffb6ba863 100644 --- a/webapp/src/cljc/lipas/schema/lois.cljc +++ b/webapp/src/cljc/lipas/schema/lois.cljc @@ -24,16 +24,19 @@ (into [:map {:description (str cat-k " > " (:value type-v)) :title (-> type-v :label :en)} - [:id [:string ]] - [:event-date [:string]] + [:id loi-id] + [:event-date {:description "Timestamp when this information became valid (ISO 8601, UTC time zone)"} + #'common/iso8601-timestamp] #_[:created-at [:string]] [:geometries (case (:geom-type type-v) ("Polygon") #'common/polygon-feature-collection ("LineString") #'common/line-string-feature-collection #'common/point-feature-collection)] [:status common/status] - [:loi-category [:enum cat-k]] - [:loi-type [:enum (:value type-v)]]] + [:loi-category {:description "The category of the type of the Location of Interest"} + [:enum cat-k]] + [:loi-type {:description "The type of the Location of Interest"} + [:enum (:value type-v)]]] (for [[prop-k prop-v] (:props type-v)] [prop-k {:optional true} (:schema prop-v)]))]))) diff --git a/webapp/src/cljc/lipas/schema/sports_sites.cljc b/webapp/src/cljc/lipas/schema/sports_sites.cljc index c901f1516..3606a39f5 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites.cljc @@ -1,6 +1,5 @@ (ns lipas.schema.sports-sites (:require [lipas.data.admins :as admins] - [lipas.data.cities :as cities] [lipas.data.owners :as owners] [lipas.data.activities :as activities] [lipas.data.prop-types :as prop-types] @@ -17,46 +16,6 @@ (def lipas-id [:int {:min 0 :label "Lipas-id" :description "Unique identifier of sports facility in LIPAS system."}]) -(def city-code - (into [:enum {:title "CityCode" - :description "Official municipality identifier https://stat.fi/fi/luokitukset/kunta/kunta_1_20240101"}] - (sort (keys cities/by-city-code)))) - -(def city-codes - [:set {:title "CityCodes" - :description (-> city-code second :description)} - city-code]) - -(defn make-location-schema [feature-schema] - [:map {:description "Location of the sports facility."} - [:city - [:map - [:city-code #'city-code] - [:neighborhood {:optional true} #'location-schema/neighborhood]]] - [:address #'location-schema/address] - [:postal-code #'location-schema/postal-code] - [:postal-office {:optional true} #'location-schema/postal-code] - [:geometries - [:map - [:type [:enum "FeatureCollection"]] - [:features - [:vector feature-schema]]]]]) - -(def line-string-feature-props - [:map - [:name {:optional true} :string] - [:lipas-id {:optional true} #'lipas-id] - [:type-code {:optional true} :int] - [:route-part-difficulty {:optional true} :string] - [:travel-direction {:optional true} :string]]) - -(def line-string-feature - (mu/assoc common/line-string-feature :properties line-string-feature-props)) - -(def point-location (make-location-schema common/point-feature)) -(def line-string-location (make-location-schema line-string-feature)) -(def polygon-location (make-location-schema common/polygon-feature)) - (def owner (into [:enum {:title "Onwer" :description "Owner entity of the sports facility."}] @@ -130,6 +89,8 @@ :description description :closed false} [:lipas-id #'lipas-id] + [:event-date {:description "Timestamp when this information became valid (ISO 8601, UTC time zone)"} + #'common/iso8601-timestamp] [:status #'common/status] [:name #'name] [:marketing-name {:optional true} #'marketing-name] @@ -162,9 +123,9 @@ :description (get-in x [:description :en]) :type-codes #{type-code} :location-schema (case geometry-type - "Point" #'point-location - "LineString" #'line-string-location - "Polygon" #'polygon-location) + "Point" #'location-schema/point-location + "LineString" #'location-schema/line-string-location + "Polygon" #'location-schema/polygon-location) :extras-schema (cond-> [:map] (seq props) (conj [:properties diff --git a/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc index 4d10ff09e..8810ca015 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites/activities.cljc @@ -1,5 +1,8 @@ (ns lipas.schema.sports-sites.activities - (:require [lipas.data.activities :as activities-data])) + (:require [clojure.string :as str] + [lipas.data.activities :as activities-data] + [lipas.data.types :as types] + [malli.util :as mu])) (def activity (into [:enum] (keys activities-data/activities))) (def activities @@ -7,10 +10,39 @@ :description "Enriched activity related content for Luontoon.fi service. Certain sports facility types may contain data about activities that can be practiced at the facility."} activity]) -(def fishing activities-data/fishing-schema) -(def outdoor-recreation-areas activities-data/outdoor-recreation-areas-schema) -(def outdoor-recreation-routes activities-data/outdoor-recreation-routes-schema) -(def outdoor-recreation-facilities activities-data/outdoor-recreation-facilities-schema) -(def cycling activities-data/cycling-schema) -(def paddling activities-data/paddling-schema) -(def birdwatching activities-data/birdwatching-schema) +(defn -append-description + [schema {:keys [type-codes label]}] + (let [s (str (:en label) + " is an activity associated with facility types " + (str/join ", " (for [[k m] (select-keys types/all type-codes)] + (str k " " (get-in m [:name :en])))) + ". Enriched activity information is collected for Luontoon.fi service.")] + (mu/update-properties schema assoc :description s))) + +(def fishing (-append-description + activities-data/fishing-schema + activities-data/fishing)) + +(def outdoor-recreation-areas (-append-description + activities-data/outdoor-recreation-areas-schema + activities-data/outdoor-recreation-areas)) + +(def outdoor-recreation-routes (-append-description + activities-data/outdoor-recreation-routes-schema + activities-data/outdoor-recreation-routes)) + +(def outdoor-recreation-facilities (-append-description + activities-data/outdoor-recreation-facilities-schema + activities-data/outdoor-recreation-facilities)) + +(def cycling (-append-description + activities-data/cycling-schema + activities-data/cycling)) + +(def paddling (-append-description + activities-data/paddling-schema + activities-data/paddling)) + +(def birdwatching (-append-description + activities-data/birdwatching-schema + activities-data/birdwatching)) diff --git a/webapp/src/cljc/lipas/schema/sports_sites/location.cljc b/webapp/src/cljc/lipas/schema/sports_sites/location.cljc index bf25fce44..502092e6c 100644 --- a/webapp/src/cljc/lipas/schema/sports_sites/location.cljc +++ b/webapp/src/cljc/lipas/schema/sports_sites/location.cljc @@ -1,5 +1,8 @@ (ns lipas.schema.sports-sites.location - (:require [lipas.schema.core :as specs])) + (:require [lipas.schema.core :as specs] + [lipas.data.cities :as cities] + [lipas.schema.common :as common] + [malli.util :as mu])) (def address [:string {:description "Street address of the sports facility." @@ -19,3 +22,46 @@ [:string {:description "Neighborhood or common name for the area of the location." :min 1 :max 100}]) + + +(def city-code + (into [:enum {:title "CityCode" + :description "Official municipality identifier https://stat.fi/fi/luokitukset/kunta/kunta_1_20240101"}] + (sort (keys cities/by-city-code)))) + +(def city-codes + [:set {:title "CityCodes" + :description (-> city-code second :description)} + city-code]) + +(defn make-location-schema [feature-schema geom-type] + [:map {:description (str "Location of the sports facility with required " + geom-type + " feature.")} + [:city + [:map + [:city-code #'city-code] + [:neighborhood {:optional true} #'neighborhood]]] + [:address #'address] + [:postal-code #'postal-code] + [:postal-office {:optional true} #'postal-code] + [:geometries + [:map + [:type [:enum "FeatureCollection"]] + [:features + [:vector feature-schema]]]]]) + +(def line-string-feature-props + [:map + [:name {:optional true} :string] + #_[:lipas-id {:optional true} #'lipas-id] + [:type-code {:optional true} :int] + [:route-part-difficulty {:optional true} :string] + [:travel-direction {:optional true} :string]]) + +(def line-string-feature + (mu/assoc common/line-string-feature :properties line-string-feature-props)) + +(def point-location (make-location-schema common/point-feature "Point")) +(def line-string-location (make-location-schema line-string-feature "LineString")) +(def polygon-location (make-location-schema common/polygon-feature "Polygon")) diff --git a/webapp/test/clj/lipas/timestamps_test.clj b/webapp/test/clj/lipas/timestamps_test.clj new file mode 100644 index 000000000..60c36948f --- /dev/null +++ b/webapp/test/clj/lipas/timestamps_test.clj @@ -0,0 +1,57 @@ +(ns lipas.timestamps-test + (:require [clojure.test :refer [deftest testing is]] + [lipas.schema.common :as common-schema])) + +(def iso8601-pattern common-schema/-iso8601-pattern) + +(deftest iso8601-timestamp-test + (testing "Valid UTC timestamps" + (is (re-matches iso8601-pattern "2024-12-27T13:25:00Z")) + (is (re-matches iso8601-pattern "2024-12-27T13:25:00.123Z")) + (is (re-matches iso8601-pattern "2024-01-01T00:00:00Z")) + (is (re-matches iso8601-pattern "2024-12-31T23:59:59Z")) + (is (re-matches iso8601-pattern "2024-02-29T00:00:00Z")) ; Valid leap year + (is (re-matches iso8601-pattern "2000-02-29T00:00:00Z"))) ; Valid century leap year + + (testing "Invalid timestamps" + ; Non-UTC timestamps (should all fail) + (is (nil? (re-matches iso8601-pattern "2024-12-27T13:25:00+02:00"))) + (is (nil? (re-matches iso8601-pattern "2024-12-27T13:25:00-02:00"))) + (is (nil? (re-matches iso8601-pattern "2024-12-27T13:25:00.123+02:00"))) + + ; Invalid dates + (is (nil? (re-matches iso8601-pattern "2024-13-01T00:00:00Z"))) ; Invalid month + (is (nil? (re-matches iso8601-pattern "2024-00-01T00:00:00Z"))) ; Invalid month + (is (nil? (re-matches iso8601-pattern "2024-12-32T00:00:00Z"))) ; Invalid day + (is (nil? (re-matches iso8601-pattern "2024-12-00T00:00:00Z"))) ; Invalid day + (is (nil? (re-matches iso8601-pattern "2023-02-29T00:00:00Z"))) ; Invalid leap year date + + ; Invalid times + (is (nil? (re-matches iso8601-pattern "2024-12-27T24:00:00Z"))) ; Invalid hour + (is (nil? (re-matches iso8601-pattern "2024-12-27T12:60:00Z"))) ; Invalid minute + (is (nil? (re-matches iso8601-pattern "2024-12-27T12:00:60Z"))) ; Invalid second + + ; Invalid formats + (is (nil? (re-matches iso8601-pattern "2024-12-27 12:00:00Z"))) ; Space instead of T + (is (nil? (re-matches iso8601-pattern "2024-12-27T12:00:00"))) ; Missing Z + (is (nil? (re-matches iso8601-pattern "24-12-27T12:00:00Z"))) ; Two-digit year + (is (nil? (re-matches iso8601-pattern "2024/12/27T12:00:00Z")))) ; Wrong separators + + (testing "Edge cases for months with different lengths" + (is (re-matches iso8601-pattern "2024-01-31T00:00:00Z")) ; January 31 valid + (is (nil? (re-matches iso8601-pattern "2024-04-31T00:00:00Z"))) ; April 31 invalid + (is (re-matches iso8601-pattern "2024-04-30T00:00:00Z")) ; April 30 valid + (is (nil? (re-matches iso8601-pattern "2024-02-30T00:00:00Z"))) ; February 30 invalid + (is (re-matches iso8601-pattern "2024-06-30T00:00:00Z")) ; June 30 valid + (is (nil? (re-matches iso8601-pattern "2024-06-31T00:00:00Z")))) ; June 31 invalid + + (testing "Fractional seconds variations" + (is (re-matches iso8601-pattern "2024-12-27T13:25:00.1Z")) + (is (re-matches iso8601-pattern "2024-12-27T13:25:00.12Z")) + (is (re-matches iso8601-pattern "2024-12-27T13:25:00.123Z")) + (is (re-matches iso8601-pattern "2024-12-27T13:25:00.1234567890Z")))) + + +(comment + (clojure.test/run-tests *ns*) + )