### Simple Coercion Example Source: https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md Demonstrates simple coercion of a string to an integer using `st/coerce` with `string-transformer` and `json-transformer`. ```clojure (st/coerce int? "1" st/string-transformer) ; 1 ``` ```clojure (st/coerce int? "1" st/json-transformer) ; "1" ``` -------------------------------- ### Spec-driven Transformation Example Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates using `:encode/*` and `:decode/*` keys within a spec for string transformations. Shows basic decode, invalid decode, decode with string-transformer, and encode/decode cycle. ```clojure (require '[clojure.spec.alpha :as s]) (require '[spec-tools.core :as st]) (require '[clojure.string :as str]) (s/def ::spec (st/spec {:spec #(and (simple-keyword? %) (-> % name str/lower-case keyword (= %))) :description "a lowercase keyword, encoded in uppercase in string-mode" :decode/string #(-> %2 name str/lower-case keyword) :encode/string #(-> %2 name str/upper-case)})) (st/decode ::spec :kikka) ; :kikka (as-> "KiKka" $ (st/decode ::spec $)) ; :clojure.spec.alpha/invalid (as-> "KiKka" $ (st/decode ::spec $ st/string-transformer)) ; :kikka (as-> "KiKka" $ (st/decode ::spec $ st/string-transformer) (st/encode ::spec $ st/string-transformer)) ; "KIKKA" ``` -------------------------------- ### Generate OpenAPI3 Spec with Parameter Transformations Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Example of generating an OpenAPI3 spec map by defining parameters using specs and the ::openapi/parameters key. ```clojure (require '[clojure.spec.alpha :as s]) (s/def ::id int?) (s/def ::name string?) (s/def ::street string?) (s/def ::city (s/nilable #{:tre :hki})) (s/def ::filters (s/coll-of string? :into [])) (s/def ::address (s/keys :req-un [::street ::city])) (s/def ::user (s/keys :req-un [::id ::name ::address])) (s/def ::token string?) (openapi/openapi-spec {:path {"/test" {:parameters [{:name "username" :in "path" :description "username to fetch" :required true :schema {:type "string"} :style "simple"} {:name "name" :in "query" :description "This will be overridden" :required true :schema {:type "string"} :style "simple"}] ::openapi/parameters {:path (s/keys :req-un [::id]) :query (s/keys :req-un [::name] :opt-un [::street ::city ::filters]) :header ::user}}}) ;; => ;; {:path ;; {"/test" ;; {:parameters ;; [{:name "username", ;; :in "path", ;; :description "username to fetch", ;; :required true, ;; :schema {:type "string"}, ;; :style "simple"} ;; {:name "id", ;; :in "path", ;; :description "", ;; :required true, ;; :schema {:type "integer", :format "int64"}} ;; {:name "name", ;; :in "query", ;; :description "", ;; :required true, ;; :schema {:type "string"}} ;; {:name "street", ;; :in "query", ;; :description "", ;; :required false, ;; :schema {:type "string"}} ;; {:name "city", ;; :in "query", ;; :description "", ;; :required false, ;; :schema {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}} ;; {:name "filters", ;; :in "query", ;; :description "", ;; :required false, ;; :schema {:type "array", :items {:type "string"}}} ;; {:name "id", ;; :in "header", ;; :description "", ;; :required true, ;; :schema {:type "integer", :format "int64"}} ;; {:name "name", ;; :in "header", ;; :description "", ;; :required true, ;; :schema {:type "string"}} ;; {:name "address", ;; :in "header", ;; :description "", ;; :required true, ;; :schema ;; {:type "object", ;; :properties ;; {"street" {:type "string"}, ;; "city" {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"}}]}}} ``` ```clojure (require '[clojure.spec.alpha :as s]) ``` -------------------------------- ### Example of incoming JSON data with extra fields Source: https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md This snippet shows an example of `evil-json-order`, which represents data received from a client that includes extra, unexpected fields not defined in the target spec. ```clojure (def evil-json-order (->> (-> order (assoc :owner "ikitommi") (assoc :LONGSTRING ".................................") (assoc-in [:items 1 :discount] 80) (assoc-in [:items 2 ::discount] 80)) (m/encode "application/json") (m/decode "application/json"))) evil-json-order ;{:id 123 ; :owner "ikitommi" ; :LONGSTRING "................................." ; :items {:1 {:description "vadelmalimsa" ; :tags ["good" "red"] ; :discount 80 ; :amount 10}, ; :2 {:description "korvapuusti" ; :tags ["raisin" "sugar"] ; ::discount 80 ; :amount 20}} ; :delivery "2007-11-20T22:19:17+02:00" ; :location [61.499374 23.7408149]} ``` -------------------------------- ### Get All Pets Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Retrieves all pets accessible to the user. ```APIDOC ## GET /api/ping ### Description Returns all pets from the system that the user has access to. ### Method GET ### Endpoint /api/ping ### Responses #### Success Response (200) - **application/xml** - Schema for XML response. - **application/json** - Schema for JSON response. ### Response Example (JSON) ```json { "street": "string", "city": { "oneOf": [ { "enum": [ "tre", "hki" ], "type": "string" }, { "type": "null" } ] } } ``` ``` -------------------------------- ### Generating Full OpenAPI 3 Specification Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Construct a complete OpenAPI 3 specification by providing all necessary components like info, servers, components (schemas and headers), and paths. This example demonstrates defining an API with multiple endpoints and request/response structures. ```clojure (openapi/openapi-spec {:openapi "3.0.3" :info {:title "Sample Pet Store App" :description "This is a sample server for a pet store." :termsOfService "http://example.com/terms/" :contact {:name "API Support", :url "http://www.example.com/support" :email "support@example.com"} :license {:name "Apache 2.0", :url "https://www.apache.org/licenses/LICENSE-2.0.html"} :version "1.0.1"} :servers [{:url "https://development.gigantic-server.com/v1" :description "Development server"} {:url "https://staging.gigantic-server.com/v1" :description "Staging server"} {:url "https://api.gigantic-server.com/v1" :description "Production server"}] :components {::openapi/schemas {:user ::user :address ::address} ::openapi/headers {:token ::token}} :paths {"/api/ping" {:get {:description "Returns all pets from the system that the user has access to" :responses {200 {::openapi/content {"application/xml" ::user "application/json" (st/spec {:spec ::address :openapi/example "Some examples here" :openapi/examples {:admin {:summary "Admin user" :description "Super user" :value {:anything :here} :externalValue "External value"}} :openapi/encoding {:contentType "application/json"}})}}}} "/user/:id" {:post {:tags ["user"] :description "Returns pets based on ID" :summary "Find pets by ID" :operationId "getPetsById" :requestBody {::openapi/content {"application/json" ::user}} :responses {200 {:description "pet response" ::openapi/content {"application/json" ::user}} :default {:description "error payload", ::openapi/content {"text/html" ::user}}} ::openapi/parameters {:path (s/keys :req-un [::id]) :header (s/keys :req-un [::token])}}}}) ;; => ;; {:openapi "3.0.3", ;; :info ;; {:title "Sample Pet Store App", ;; :description "This is a sample server for a pet store.", ;; :termsOfService "http://example.com/terms/", ;; :contact ;; {:name "API Support", ;; :url "http://www.example.com/support", ;; :email "support@example.com"}, ;; :license ;; {:name "Apache 2.0", ;; :url "https://www.apache.org/licenses/LICENSE-2.0.html"}, ;; :version "1.0.1"}, ;; :servers ;; [{:url "https://development.gigantic-server.com/v1", ;; :description "Development server"} ;; {:url "https://staging.gigantic-server.com/v1", ;; :description "Staging server"} ;; {:url "https://api.gigantic-server.com/v1", ;; :description "Production server"}], ;; :components ;; {:schemas ;; {:user ;; {:type "object", ;; :properties ;; {:id {:type "integer", :format "int64"}, ;; :name {:type "string"}, ;; :address ;; {:type "object", ;; :properties ;; {:street {:type "string"}, ;; :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"}}, ;; :required ["id" "name" "address"], ;; :title "spec-tools.openapi.core-test/user"} ;; :address ;; {:type "object", ;; :properties ;; {:street {:type "string"}, ;; :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"} ;; :token {:type "string"}}, ;; :headers {:token {:type "string"}}}, ;; :paths ;; {"/api/ping" ;; {:get ;; {:description "Returns all pets from the system that the user has access to", ;; :responses ;; {200 ;; {:content ;; {"application/xml" {:spec {:type "object", :properties {:id {:type "integer", :format "int64"}, :name {:type "string"}, :address {:type "object", :properties {:street {:type "string"}, :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, :required ["street" "city"], :title "spec-tools.openapi.core-test/address"}}, :required ["id" "name" "address"], :title "spec-tools.openapi.core-test/user"}} ;; "application/json" ;; {:schema ;; {:type "object", ;; :properties ;; {:street {:type "string"}, ;; :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"}, ;; :example "Some examples here", ;; :examples ;; {:admin ;; {:summary "Admin user", ;; :description "Super user", ;; :value {:anything :here}, ;; :externalValue "External value"}}, ;; :encoding {:contentType "application/json"}}}}} ;; "/user/:id" ;; {:post ;; {:tags ["user"], ;; :description "Returns pets based on ID", ;; :summary "Find pets by ID", ;; :operationId "getPetsById", ;; :requestBody ;; {:content {"application/json" ;; {:spec {:type "object", :properties {:id {:type "integer", :format "int64"}, :name {:type "string"}, :address {:type "object", :properties {:street {:type "string"}, :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, :required ["street" "city"], :title "spec-tools.openapi.core-test/address"}}, :required ["id" "name" "address"], :title "spec-tools.openapi.core-test/user"}}}} ;; :responses ;; {200 ;; {:description "pet response", ;; :content ;; {"application/json" ;; {:spec {:type "object", :properties {:id {:type "integer", :format "int64"}, :name {:type "string"}, :address {:type "object", :properties {:street {:type "string"}, :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, :required ["street" "city"], :title "spec-tools.openapi.core-test/address"}}, :required ["id" "name" "address"], :title "spec-tools.openapi.core-test/user"}}} ;; } ;; :default ;; {:description "error payload", ;; :content {"text/html" ;; {:spec {:type "object", :properties {:id {:type "integer", :format "int64"}, :name {:type "string"}, :address {:type "object", :properties {:street {:type "string"}, :city {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, :required ["street" "city"], :title "spec-tools.openapi.core-test/address"}}, :required ["id" "name" "address"], :title "spec-tools.openapi.core-test/user"}}}}} ;; :parameters ;; {:path {:spec {:type "object", :required ["id"], :properties {"id" {:type "string"}}}} ;; :header {:spec {:type "object", :required ["token"], :properties {"token" {:type "string"}}}}} ;; }}} ``` -------------------------------- ### Collect Spec Forms with Visitor Source: https://github.com/metosin/spec-tools/blob/master/docs/03_spec_visitor.md Use the `visitor/visit` function to recursively collect all registered spec forms within a given spec. This example demonstrates collecting spec definitions into an atom. ```clojure (require '[clojure.spec.alpha :as s]) (require '[spec-tools.visitor :as visitor]) (s/def ::id string?) (s/def ::name string?) (s/def ::street string?) (s/def ::city #{:tre :hki}) (s/def ::address (s/keys :req-un [::street ::city])) (s/def ::user (s/keys :req-un [::id ::name ::address])) ;; visitor to recursively collect all registered spec forms (let [specs (atom {})] (visitor/visit ::user (fn [_ spec _ _] (if-let [s (s/get-spec spec)] (swap! specs assoc spec (s/form s)) @specs)))) ``` -------------------------------- ### Composing Transformers with Type-Based Mappings Source: https://github.com/metosin/spec-tools/blob/master/docs/10_spec_transformations.md Demonstrates composing transformers by merging type-based decoders and encoders. This example shows how to create a `strict-json-transformer` by combining JSON and key-stripping functionalities. ```clojure (def strict-json-transformer (st/type-transformer {:name :custom :decoders (merge stt/json-type-decoders stt/strip-extra-keys-type-decoders) :encoders stt/json-type-encoders})) ``` -------------------------------- ### Generate OpenAPI Content Definitions Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Use `openapi/openapi-spec` with `::openapi/content` to define OpenAPI Media Type Objects. Supports specifying schemas, examples, and encodings per content type. ```clojure (openapi/openapi-spec {:content {"test/html" {:schema {:type "string"}} "application/json" {:schema {:type "null"} :example "Will be overridden"}} ::openapi/content {"application/json" (st/spec {:spec ::user :openapi/example "Some examples here" :openapi/examples {:admin {:summary "Admin user" :description "Super user" :value {:anything :here} :externalValue "External value"}} :openapi/encoding {:contentType "application/json"}}) "application/xml" ::address "*/*" (s/keys :req-un [::id ::name] :opt-un [::street ::filters])}) ;; => ;; {:content ;; {"test/html" {:schema {:type "string"}}, ;; "application/json" ;; {:schema ;; {:type "object", ;; :properties ;; {"id" {:type "integer", :format "int64"}, ;; "name" {:type "string"}, ;; "address" ;; {:type "object", ;; :properties ;; {"street" {:type "string"}, ;; "city" {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"}}, ;; :required ["id" "name" "address"], ;; :title "spec-tools.openapi.core-test/user", ;; :example "Some examples here", ;; :examples ;; {:admin ;; {:summary "Admin user", ;; :description "Super user", ;; :value {:anything :here}, ;; :externalValue "External value"}}, ;; :encoding {:contentType "application/json"}}}, ;; "application/xml" ;; {:schema ;; {:type "object", ;; :properties ;; {"street" {:type "string"}, ;; "city" {:oneOf [{:enum [:tre :hki], :type "string"} {:type "null"}]}}, ;; :required ["street" "city"], ;; :title "spec-tools.openapi.core-test/address"}}, ;; "*/*" ;; {:schema ;; {:type "object", ;; :properties ;; {"id" {:type "integer", :format "int64"}, ;; "name" {:type "string"}, ;; "street" {:type "string"}, ;; "filters" {:type "array", :items {:type "string"}}}, ;; :required ["id" "name"]}}}} ``` -------------------------------- ### Spec-Based Transformations with Custom Encode/Decode Source: https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md Embed custom `:decode/string` and `:encode/string` functions within spec annotations for specific data transformations. This example defines a keyword that is lowercased on decode and uppercased on encode. ```clojure (require '[clojure.string :as str]) (s/def ::my-keyword (st/spec {:spec #(and (simple-keyword? %) (-> % name str/lower-case keyword (= %))) :description "a lowercase keyword, uppercase in string-mode" :json-schema/type {:type "string", :format "keyword"} :json-schema/example "kikka" :decode/string #(-> %2 name str/lower-case keyword) :encode/string #(-> %2 name str/upper-case)})) (st/coerce ::my-keyword "OLIPA.KERRAN/AVARUUS" st/string-transformer) ; :olipa.kerran/avaruus ``` -------------------------------- ### Generating Content Objects with `::openapi/content` Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md This function generates OpenAPI content objects, mapping content types to their respective media type objects. It allows specifying schemas, examples, and encodings for different content types. ```APIDOC ## `::openapi/content` ### Description Generates OpenAPI content objects, which map content-type strings to OpenAPI 3 Media Type objects. This is useful for defining the structure and examples of request or response bodies. ### Parameters This function operates on a map structure that includes a `::openapi/content` key. The value associated with this key is a map where keys are content-type strings (e.g., `"application/json"`) and values are the corresponding Media Type objects. ### Returns A map with a single key `:content`, whose value is a map of content-type strings to OpenAPI 3 Media Type objects. ### Example ```clojure (openapi/openapi-spec {:content {"test/html" {:schema {:type "string"}}} ::openapi/content {"application/json" (st/spec {:spec ::user :openapi/example "Some examples here" :openapi/examples {:admin {:summary "Admin user" :description "Super user" :value {:anything :here} :externalValue "External value"}} :openapi/encoding {:contentType "application/json"}}) "application/xml" ::address "*/*" (s/keys :req-un [::id ::name] :opt-un [::street ::filters])}) ``` ``` -------------------------------- ### Transform Annotated Spec to Swagger Schema Source: https://github.com/metosin/spec-tools/blob/master/docs/05_swagger.md Converts an annotated Clojure spec into a Swagger2 schema, populating fields like :title, :description, :example, and :default from spec annotations. ```clojure (require '[spec-tools.core :as st]) (swagger/transform (st/spec {:spec integer? :name "integer" :description "it's an int" :swagger/example 42 :json-schema/default 42})) ;{:type "integer" ; :title "integer" ; :description "it's an int" ; :example 42 ; :default 42} ``` -------------------------------- ### Creating Spec Records Source: https://github.com/metosin/spec-tools/blob/master/docs/07_spec_records.md Demonstrates various ways to create Spec Records, including using type inference, explicit types, map forms, and the `create-spec` function. The resulting Spec Record wraps a predicate and can include metadata. ```clojure (require '[spec-tools.core :as st]) ;; using type inference (st/spec integer?) ;; with explicit type (st/spec integer? {:type :long}) ;; map form (st/spec {:spec integer?}) (st/spec {:spec integer?, :type :long}) ;; function (st/create-spec {:spec integer? :form `integer? :type :long}) ;; function, with type and form inference (st/create-spec {:spec integer?}) ;; ... resulting in: ; #Spec{:type :long, ; :form clojure.core/integer?} ``` -------------------------------- ### Using a Custom Spec Record Source: https://github.com/metosin/spec-tools/blob/master/docs/07_spec_records.md Shows how to use a custom Spec Record, check its validity, and associate additional metadata like a description. The `assoc` function can be used to add keys to an existing Spec Record. ```clojure (require '[clojure.spec.alpha :as s]) (def my-integer? (st/spec integer?)) my-integer? ; #Spec{:type :long ; :form clojure.core/integer?} (my-integer? 1) ; true (s/valid? my-integer? 1) ; true (assoc my-integer? :description "It's a int") ; #Spec{:type :long ; :form clojure.core/integer? ; :description "It's a int"} (eval (s/form (st/spec integer? {:description "It's a int"}))) ; #Spec{:type :long ; :form clojure.core/integer? ; :description "It's a int"} ``` -------------------------------- ### Type-driven Transformation for Instants Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates decoding and encoding of ISO8601 date-time strings to Clojure instants using the string transformer. Shows initial invalid decode and subsequent successful transformations. ```clojure (as-> "2014-02-18T18:25:37Z" $ (st/decode inst? $)) ; :clojure.spec.alpha/invalid ;; decode using string-transformer (as-> "2014-02-18T18:25:37Z" $ (st/decode inst? $ st/string-transformer)) ; #inst"2014-02-18T18:25:37.000-00:00" ;; encode using string-transformer (as-> "2014-02-18T18:25:37Z" $ (st/decode inst? $ st/string-transformer) (st/encode inst? $ st/string-transformer)) ; "2014-02-18T18:25:37.000+0000" ``` -------------------------------- ### Specifying Optional Keys with ds/spec and :keys-default Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Illustrates using `ds/spec` with the `:keys-default ds/opt` option to define map specs where keys are optional by default. Useful for flexible input schemas. ```clojure (require '[clojure.spec.alpha :as s]) (require '[spec-tools.data-spec :as ds]) (s/valid? (ds/spec {:name ::optiona-user :spec {(ds/req :id) int? :age pos-int? :name string?} :keys-default ds/opt}) {:id 123}) ; true ``` -------------------------------- ### Predefined Spec Record for Boolean Source: https://github.com/metosin/spec-tools/blob/master/docs/07_spec_records.md Illustrates the usage of a predefined Spec Record for the `boolean?` predicate from `spec-tools.spec`. It shows how to access the Spec Record, use it for validation, and associate a description. ```clojure (require '[spec-tools.spec :as spec]) spec/boolean? ; #Spec{:type :boolean ; :form clojure.core/boolean?} (spec/boolean? true) ; true (s/valid? spec/boolean? false) ; true (assoc spec/boolean? :description "it's an bool") ; #Spec{:type :boolean ; :form clojure.core/boolean? ; :description "It's a bool"} ``` -------------------------------- ### Inspect Registered Specs Source: https://github.com/metosin/spec-tools/blob/master/docs/02_data_specs.md Use `keys` with a regex to view the specs that have been registered in the registry. ```clojure (keys (st/registry #"user.*?")) ``` -------------------------------- ### Define Sample Order Data Source: https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md Defines a sample `::order` data structure and validates it against the defined specs. ```clojure (def order {:id 123, :items {1 {:description "vadelmalimsa" :tags #{:good :red} :amount 10}, 2 {:description "korvapuusti" :tags #{:raisin :sugar} :amount 20}}, :delivery #inst"2007-11-20T20:19:17.000-00:00", :location [61.499374 23.7408149]}) (s/valid? ::order order) ; true ``` -------------------------------- ### Dependency Versions in 0.7.1 Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Lists the specific versions of dependencies used in release 0.7.1, noting available newer versions. ```clojure [com.fasterxml.jackson.core/jackson-databind "2.9.6"] is available but we use "2.9.5" ``` -------------------------------- ### Using ds/or for Union of Specs Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates how to use `ds/or` to create a spec that accepts either a map with a string alias or a plain string. Useful for defining flexible data structures. ```clojure (require '[clojure.spec.alpha :as s]) (require '[spec-tools.data-spec :as ds]) (s/conform (ds/spec ::user [(ds/or {:map {:alias string?} :string string?})]) [{:alias "Rudi"}, "Rudolf"]) ; [[:map {:alias "rudi"}] [:string "Rudolf"]] ``` -------------------------------- ### Parsing s/and Specification Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates how `parse/parse-spec` represents an `s/and` specification, showing the parsed items and their types. ```clojure (testing "s/and" (is (= {::parse/items [{:spec int?, :type :long} {:spec keyword?, :type :keyword}] :type [:and [:long :keyword]]} (parse/parse-spec (s/and int? keyword?))))) ``` -------------------------------- ### Dependency Updates in 0.5.1 Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Shows the Clojure and spec.alpha versions used in release 0.5.1. ```clojure [org.clojure/clojure "1.9.0-beta4"] is available but we use "1.9.0-beta4" [org.clojure/spec.alpha "0.1.143"] is available but we use "0.1.134" ``` -------------------------------- ### Dependency Versions in 0.7.2 Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Lists the specific versions of dependencies used in release 0.7.2, noting available newer versions. ```clojure [org.clojure/spec.alpha "0.2.176"] is available but we use "0.1.143" [org.clojure/clojurescript "1.10.339"] is available but we use "1.10.329" [com.fasterxml.jackson.core/jackson-databind "2.9.7"] is available but we use "2.9.6" ``` -------------------------------- ### Run Tests with Test Script Source: https://github.com/metosin/spec-tools/blob/master/docs/11_developer_guide.md Execute project tests using the provided shell script. Supports both Clojure (clj) and ClojureScript (cljs) test environments. ```shell ./scripts/test.sh clj ``` ```shell ./scripts/test.sh cljs ``` -------------------------------- ### Defining a Custom String Transformer Source: https://github.com/metosin/spec-tools/blob/master/docs/10_spec_transformations.md Illustrates the creation of a custom transformer `my-string-transformer` that includes a custom keyword decoder and a default string encoder. This allows for specialized data transformations. ```clojure (require '[clojure.string :as str]) (require '[spec-tools.transform :as stt]) (defn transform [_ value] (-> value str/upper-case str/reverse keyword)) ;; string-decoding + special keywords ;; encoding writes strings by default (def my-string-transformer (type-transformer {:name :custom :decoders (merge stt/string-type-decoders {:keyword transform}) :default-encoder stt/any->string})) (decode keyword? "kikka") ; ::s/invalid (decode keyword? "kikka" my-string-transformer) ; :AKKIK ; spec-driven transforming (decode (spec {:spec #(keyword? %) :decode/custom transform}) "kikka" my-string-transformer) ; :AKKIK ;; defaut encoding to strings (encode int? 1 my-string-transformer) ; "1" ``` -------------------------------- ### Coercion Tests with Various Specs Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates the usage of `st/coerce` with different spec types like predicates, s/and, s/or, s/coll-of, s/keys, s/map-of, s/nilable, and s/every. It shows how values are transformed using string and JSON transformers. ```clojure (deftest coercion-test (testing "predicates" (is (= 1 (st/coerce int? "1" st/string-transformer))) (is (= "1" (st/coerce int? "1" st/json-transformer))) (is (= :user/kikka (st/coerce keyword? "user/kikka" st/string-transformer)))) (testing "s/and" (is (= 1 (st/coerce (s/and int? keyword?) "1" st/string-transformer))) (is (= :1 (st/coerce (s/and keyword? int?) "1" st/string-transformer)))) (testing "s/or" (is (= 1 (st/coerce (s/or :int int? :keyword keyword?) "1" st/string-transformer))) (is (= :1 (st/coerce (s/or :keyword keyword? :int int?) "1" st/string-transformer)))) (testing "s/coll-of" (is (= #{1 2 3} (st/coerce (s/coll-of int? :into #{}) ["1" 2 "3"] st/string-transformer))) (is (= #{"1" 2 "3"} (st/coerce (s/coll-of int? :into #{}) ["1" 2 "3"] st/json-transformer))) (is (= [:1 2 :3] (st/coerce (s/coll-of keyword?) ["1" 2 "3"] st/string-transformer))) (is (= ::invalid (st/coerce (s/coll-of keyword?) ::invalid st/string-transformer)))) (testing "s/keys" (is (= {:c1 1, ::c2 :kikka} (st/coerce (s/keys :req-un [::c1]) {:c1 "1", ::c2 "kikka"} st/string-transformer))) (is (= {:c1 1, ::c2 :kikka} (st/coerce (s/keys :req-un [(and ::c1 ::c2)]) {:c1 "1", ::c2 "kikka"} st/string-transformer))) (is (= {:c1 "1", ::c2 :kikka} (st/coerce (s/keys :req-un [::c1]) {:c1 "1", ::c2 "kikka"} st/json-transformer))) (is (= ::invalid (st/coerce (s/keys :req-un [::c1]) ::invalid st/json-transformer)))) (testing "s/map-of" (is (= {1 :abba, 2 :jabba} (st/coerce (s/map-of int? keyword?) {"1" "abba", "2" "jabba"} st/string-transformer))) (is (= {"1" :abba, "2" :jabba} (st/coerce (s/map-of int? keyword?) {"1" "abba", "2" "jabba"} st/json-transformer))) (is (= ::invalid (st/coerce (s/map-of int? keyword?) ::invalid st/json-transformer)))) (testing "s/nillable" (is (= 1 (st/coerce (s/nilable int?) "1" st/string-transformer))) (is (= nil (st/coerce (s/nilable int?) nil st/string-transformer)))) (testing "s/every" (is (= [1] (st/coerce (s/every int?) ["1"] st/string-transformer)))) (testing "composed" (let [spec (s/nilable (s/nilable (s/map-of keyword? (s/or :keys (s/keys :req-un [::c1]) :ks (s/coll-of (s/and int?) :into #{}))))) value {"keys" {:c1 "1" ::c2 "kikka"} "keys2" {:c1 true} "ints" [1 "1" "invalid" "3"]}] (is (= {:keys {:c1 1 ::c2 :kikka} :keys2 {:c1 true} :ints #{1 "invalid" 3}} (st/coerce spec value st/string-transformer))) (is (= {:keys {:c1 "1" ::c2 :kikka} :keys2 {:c1 true} :ints #{1 "1" "invalid" "3"}} (st/coerce spec value st/json-transformer)))))) ``` -------------------------------- ### Transform Spec to OpenAPI3 Schema (anyOf) Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Shows how to transform a spec with a sequence of types (s/cat) into an OpenAPI3 schema using 'anyOf'. ```clojure ;; OpenAPI3 support anyOf (openapi/transform (s/cat :string string? :int integer?)) ;;=> {:type "array", :items {:anyOf [{:type "integer"} {:type "string"}]}} ``` -------------------------------- ### Require Data Spec Library Source: https://github.com/metosin/spec-tools/blob/master/docs/02_data_specs.md Import the necessary data-spec library for use. ```clojure (require '[spec-tools.data-spec :as ds]) ``` -------------------------------- ### Encoding JDBC Connection to URL with Target Spec Source: https://github.com/metosin/spec-tools/blob/master/docs/10_spec_transformations.md Demonstrates encoding a JDBC connection map into a connection string using a custom transformer and specifying a target spec (`:db/conn-string`) for the output. This allows for transforming data into a specific string format. ```clojure (s/def :db/conn-string string?) (st/encode ::jdbc-connection {:hostname "127.0.0.1" :port 5432 :database "postgres"} jdbc-transformer :db/conn-string) ;; => "jdbc:postgres://127.0.0.1:5432/postgres" ``` -------------------------------- ### Using Qualified Keys with ds/spec Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates `ds/spec`'s behavior with qualified map keys, where the `:name` option is only required if non-qualified keys are present. Ensures correct spec definition for namespaced keys. ```clojure (require '[clojure.spec.alpha :as s]) (require '[spec-tools.data-spec :as ds]) (s/valid? (ds/spec {:spec [{::alias string?}]}) [{::alias "kikka"} {::alias "kukka"}]) ; true ``` -------------------------------- ### Swagger Schema Transformation with Spec Tools Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates how spec-tools merges Swagger and JSON schema properties, with Swagger properties taking precedence. ```clojure (require '[spec-tools.core :as st]) (require '[spec-tools.swagger.core :as swagger]) (swagger/transform (st/spec {:spec string? :json-schema/default "" :json-schema/example "json-schema-example" :swagger/example "swagger-example"})) ; {:type "string" ; :default "" ; :example "swagger-example"} ``` -------------------------------- ### Define Specs and Generate Swagger Parameters Source: https://github.com/metosin/spec-tools/blob/master/docs/05_swagger.md Defines Clojure specs for user data and then uses `swagger/swagger-spec` to generate Swagger parameters, including query and body parameters, from these specs. Existing parameters with the same name and in are overridden. ```clojure (require '[clojure.spec.alpha :as s]) (s/def ::id string?) (s/def ::name string?) (s/def ::street string?) (s/def ::city #{:tre :hki}) (s/def ::address (s/keys :req-un [::street ::city])) (s/def ::user (s/keys :req-un [::id ::name ::address])) (swagger/swagger-spec {:paths {"echo" {:post {:parameters [;; existing parameter, will be overriddden {:in "query" :name "name" :required false} ;; unique parameter, will remain {:in "query" :name "name2" :type "string" :required true}] ;; the spec-parameters ::swagger/parameters {:query (s/keys :opt-un [::name]) :body ::user}}}}) ``` ```clojure {:paths {"echo" {:post {:parameters [{:in "query" :name "name2" :type "string" :required true} {:in "query" :name "name" :description "" :type "string" :required false} {:in "body", :name "user/user", :description "", :required true, :schema {:type "object", :properties {"id" {:type "string"}, "name" {:type "string"}, "address" {:type "object", :properties {"street" {:type "string"}, "city" {:enum [:tre :hki], :type "string"}}, :required ["street" "city"], :title "user/address"}}, :required ["id" "name" "address"], :title "user/user"}}]}}}} ``` -------------------------------- ### Type-driven Transformation for Keywords Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Shows how to define a spec for keywords using `:type :keyword` to automatically leverage keyword encoders and decoders with different transformers. ```clojure (s/def ::kw (st/spec {:spec #(keyword %) ;; anonymous function :type :keyword})) ;; encode & decode like a keyword (st/decode ::kw "kikka" st/string-transformer) ;; :kikka (st/decode ::kw "kikka" st/json-transformer) ;; :kikka ``` -------------------------------- ### Dependency Versions Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Lists the Clojure and ClojureScript versions available and currently used by the library. ```clojure [org.clojure/clojure "1.9.0-alpha19"] is available but we use "1.9.0-alpha17" [org.clojure/clojurescript "1.9.908"] is available but we use "1.9.660" ``` -------------------------------- ### Handle Nilable Specs in Swagger2 Source: https://github.com/metosin/spec-tools/blob/master/docs/05_swagger.md Demonstrates how nilable specs are transformed in Swagger2, noting that 'null' is not directly supported and is represented using :x-nullable. ```clojure ;; no "null" in swagger2 (swagger/transform (s/nilable string?)) ; {:type "string", :x-nullable true} ``` -------------------------------- ### Defining a JDBC Connection Transformer Source: https://github.com/metosin/spec-tools/blob/master/docs/10_spec_transformations.md Illustrates creating a custom transformer `jdbc-transformer` for encoding database connection details into a JDBC URL. It includes a specific encoder for the `:dbconn` type and a default encoder. ```clojure (s/def :db/hostname string?) (s/def :db/port pos-int?) (s/def :db/database string?) (s/def ::jdbc-connection (st/spec {:spec (s/keys :req-un [:db/hostname :db/port :db/database]) :type :dbconn})) (defn dbconn->url [_ {:keys [hostname port database]}] (format "jdbc:postgres://%s:%s/%s" hostname port database)) (def jdbc-transformer (st/type-transformer {:name :jdbc :encoders {:dbconn dbconn->url} :default-encoder stt/any->any})) (st/encode ::jdbc-connection {:hostname "127.0.0.1" :port 5432 :database "postgres"} jdbc-transformer) ;; => ::s/invalid ``` -------------------------------- ### Require Spec Visitor Namespace Source: https://github.com/metosin/spec-tools/blob/master/docs/03_spec_visitor.md Import the necessary namespace for using the spec-tools visitor functionality. ```clojure (require '[spec-tools.visitor :as visitor]) ``` -------------------------------- ### Parsing s/or Specification Source: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md Demonstrates how `parse/parse-spec` represents an `s/or` specification, showing the parsed items and their types. ```clojure (testing "s/or" (is (= {::parse/items [{:spec int?, :type :long} {:spec keyword?, :type :keyword}] :type [:or [:long :keyword]]} (parse/parse-spec (s/or :int int? :keyword keyword?))))) ``` -------------------------------- ### Require OpenAPI Core Namespace Source: https://github.com/metosin/spec-tools/blob/master/docs/06_openapi.md Import the necessary OpenAPI core namespace for transformations. ```clojure (require '[spec-tools.openapi.core :as openapi]) ``` -------------------------------- ### Simulate JSON Round-tripping with Muuntaja Source: https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md Simulates a JSON round-trip using Muuntaja, encoding and then decoding the order data, highlighting potential data type issues. ```clojure (require '[muuntaja.core :as m]) (def json-order (->> order (m/encode "application/json") (m/decode "application/json"))) json-order ;{:id 123, ; :delivery "2007-11-20T20:19:17Z", ; :items {:1 {:description "vadelmalimsa" ; :tags ["good" "red"] ; :amount 10}, ; :2 {:description "korvapuusti" ; :tags ["raisin" "sugar"] ; :amount 20}} ; :location [61.499374 23.7408149]} (s/valid? ::order json-order) ; false ```