-
Notifications
You must be signed in to change notification settings - Fork 6
Mashkov Homework-15 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "name": { | ||
| "type": "string", | ||
| "minLength": 3, | ||
| "maxLength": 50 | ||
| }, | ||
| "age": { | ||
| "type": "integer", | ||
| "minimum": 18, | ||
| "maximum": 99 | ||
| }, | ||
| "email": { | ||
| "type": "string", | ||
| "format": "email" | ||
| }, | ||
| "is_active": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "required": ["name", "age", "email"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,92 @@ | ||
| (ns spec-faker.core | ||
| (:require [ring.adapter.jetty :refer [run-jetty]] | ||
| [compojure.core :refer :all] | ||
| [hiccup.core :refer [html]]) | ||
| (:gen-class)) | ||
| [cheshire.core :as json] | ||
| [compojure.api.sweet :refer :all] | ||
| [compojure.route :as route] | ||
| [spec-faker.schema-validator :refer [validate-schema explain-validation-error]] | ||
| [spec-faker.data-generator :refer [gen-example]] | ||
| [ring.util.http-response :refer :all] | ||
| [hiccup.core :refer [html]] | ||
| [schema.core :as s]) | ||
| (:gen-class) | ||
| (:import (com.fasterxml.jackson.core JsonParseException) | ||
| (java.net URLEncoder))) | ||
|
|
||
| ;; Convert JSON string to EDN | ||
| (defn json-to-edn [json-str] | ||
| (json/parse-string json-str true)) | ||
|
|
||
| (def bad-json-response | ||
| (bad-request {:error "Invalid JSON\n"})) | ||
|
|
||
| (defn invalid-spec-response [decoded-spec] | ||
| (bad-request {:error (str "Invalid spec\n" (explain-validation-error decoded-spec))})) | ||
|
|
||
| (def input-form-response | ||
| (html | ||
| [:html | ||
| [:head [:title "Spec Faker"]] | ||
| [:body | ||
| [:h1 "Insert OpenAPI specification"] | ||
| [:form {:method "post" :action "/"} | ||
| [:textarea {:name "spec" :rows 10 :cols 50}] | ||
| [:br] | ||
| [:input {:type "submit" :value "Generate"}]]]])) | ||
|
|
||
| (defn gen-data-response [spec] | ||
| (let [decoded-spec (json-to-edn spec)] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Строго говоря, тут может и ошибка возникнуть, если JSON не получится декодировать. UPD: вижу, что ниже эта ошибка обрабатывается. Но я бы ожидал отлов ошибки парсинга здесь, раз уж в этой функции вы уже responses возвращаете, в том числе и bad request. |
||
| (if (validate-schema decoded-spec) | ||
| (ok (gen-example decoded-spec)) | ||
| (invalid-spec-response decoded-spec)))) | ||
|
|
||
| (defn redirect-response [spec-json] | ||
| (see-other (str "/?spec=" (URLEncoder/encode ^String spec-json "UTF-8")))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно воспользоваться hiccup.util/url, раз уж hiccup всё равно используется в проекте |
||
|
|
||
| (defn invalid-spec-form-response [spec-json] | ||
| (let [decoded-spec (json-to-edn spec-json)] | ||
| (bad-request {:error (str "Invalid Swagger spec: " (explain-validation-error decoded-spec))}))) | ||
|
|
||
|
|
||
| ;; Define the API | ||
| (defapi app | ||
| {:swagger {:ui "/swagger-ui" | ||
| :spec "/swagger.json" | ||
| :data {:info {:title "Spec Faker API" | ||
| :description "Generate example data from OpenAPI specifications"} | ||
| :tags [{:name "faker" :description "APIs for generating example data"}]}}} | ||
|
|
||
| (context "/" [] | ||
| :tags ["faker"] | ||
|
|
||
| ; GET endpoint to fetch form or generate data | ||
| (GET "/" [] | ||
| :query-params [{spec :- (s/maybe s/Str) nil}] | ||
| (if spec | ||
| (try | ||
| (gen-data-response spec) | ||
| (catch JsonParseException _ bad-json-response)) | ||
| (content-type (ok input-form-response) "text/html"))) | ||
|
|
||
|
|
||
| ;; POST endpoint to validate and process spec | ||
| (POST "/" [] | ||
| :form-params [spec :- s/Str] | ||
| :return {:redirect s/Str} | ||
| (try | ||
| (if (validate-schema (json-to-edn spec)) | ||
| (redirect-response spec) | ||
| (invalid-spec-form-response spec)) | ||
| (catch JsonParseException _ bad-json-response)))) | ||
|
|
||
| (undocumented | ||
| (ANY "*" [] | ||
| (not-found "Not Found")))) | ||
|
|
||
| (comment | ||
| (run-jetty #'app {:join? false | ||
| :port 8000})) | ||
|
|
||
| ;; Start the server | ||
| (defn -main | ||
| [& args] | ||
| (run-jetty | ||
| (routes | ||
| (GET "/" [] | ||
| (html [:h1 "Hello World!"]))) | ||
| {:port 8000})) | ||
| (run-jetty app {:port 8000 :join? false})) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| (ns spec-faker.data-generator | ||
| (:require [clojure.test.check.generators :as gen])) | ||
|
|
||
| (defn gen-integer | ||
| "Генерирует целое число в интервале [min, max], если указаны. | ||
| Если min/max не указаны, применяем некий разумный диапазон по умолчанию." | ||
| [schema] | ||
| (let [minimum (:minimum schema 0) | ||
| maximum (:maximum schema Integer/MAX_VALUE)] | ||
|
Comment on lines
+8
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно попробовать вынести эти опции в деструктуризацию и указать значения по умолчанию там же через |
||
| (gen/choose minimum maximum))) | ||
|
|
||
| (defn gen-boolean | ||
| "Генерирует случайное boolean-значение." | ||
| [] | ||
| gen/boolean) | ||
|
|
||
| (defn gen-random-string | ||
| "Вспомогательная функция: генерирует строку из букв/цифр ограниченной длины. | ||
| При необходимости усечёт строку до max-length." | ||
| [min-len max-len] | ||
| (gen/fmap | ||
| (fn [s] | ||
| (let [cut (subs s 0 (min max-len (count s)))] | ||
| cut)) | ||
| (gen/such-that #(>= (count %) min-len) gen/string-alphanumeric))) | ||
|
|
||
| (defn gen-email | ||
| "Простейший генератор email-адреса. Игнорируем minLength/maxLength, | ||
| но по желанию можно усложнить формат." | ||
| [min-len max-len] | ||
| (gen/fmap | ||
| (fn [[user domain tld]] | ||
| (let [email (str user "@" domain "." tld)] | ||
| (subs email 0 (min (count email) max-len)))) | ||
| (gen/tuple | ||
| (gen/such-that #(>= (count %) (max 1 (dec min-len))) gen/string-alphanumeric) | ||
| (gen/such-that #(pos? (count %)) gen/string-alphanumeric) | ||
| (gen/elements ["com" "org" "net" "ru" "test"])))) | ||
|
|
||
| (defn gen-string | ||
| "Генерирует строку с учётом minLength, maxLength и формата (например, email)." | ||
| [schema] | ||
| (let [min-length (or (get schema :minLength) 0) | ||
| max-length (or (get schema :maxLength) (max 10 min-length)) ; чтобы max >= min | ||
| format (get schema :format)] | ||
| (case format | ||
| "email" (gen-email min-length max-length) | ||
| (gen-random-string min-length max-length)))) | ||
|
|
||
| (defn gen-object | ||
| "Принимает карту (properties) вида {\"propertyName\" propertySchema, ...} | ||
| Возвращает генератор, создающий Clojure map: | ||
| {:propertyName <случайное значение>}." | ||
| [properties] | ||
| (let [ | ||
| prop-gens | ||
| (map | ||
| (fn [[prop-name prop-spec]] | ||
| (let [t (:type prop-spec)] | ||
| [prop-name | ||
| (case t | ||
| "integer" (gen-integer prop-spec) | ||
| "boolean" (gen-boolean) | ||
| "string" (gen-string prop-spec) | ||
| (gen/return nil))])) | ||
| properties)] | ||
| (apply gen/hash-map (mapcat identity prop-gens)))) | ||
|
|
||
| (defn gen-schema | ||
| "Рекурсивно обходит schema и возвращает генератор данных для неё." | ||
| [schema] | ||
| (let [t (:type schema)] | ||
| (case t | ||
| "object" | ||
| (let [props (:properties schema {})] | ||
| (gen-object props)) | ||
|
|
||
| "integer" | ||
| (gen-integer schema) | ||
|
|
||
| "boolean" | ||
| (gen-boolean) | ||
|
|
||
| "string" | ||
| (gen-string schema) | ||
|
|
||
| (gen/return nil)))) | ||
|
|
||
| (defn gen-example | ||
| ([schema count] | ||
| (gen/sample (gen-schema schema) count)) | ||
| ([schema] | ||
| (first (gen-example schema 1)))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Я бы не очень хотел, чтобы функция возвращала здесь не последовательность, а один элемент, даже если count подразумевается равным единице - не очень приятно получать результаты вызова функции, имеющие разные типы без особой на то нужды. Если хочется возвращать именно один элемент, то имеет смысл завести две функции: |
||
|
|
||
|
|
||
|
|
||
| (def example-spec | ||
| {:type "object" | ||
| :properties | ||
| {:age {:type "integer" :minimum 18 :maximum 80} | ||
| :email {:type "string" :format "email" :minLength 6 :maxLength 30} | ||
| :isActive {:type "boolean"} | ||
| :nickname {:type "string" :minLength 3 :maxLength 10}}}) | ||
|
|
||
| (comment | ||
| (let [gen-example (gen-schema example-spec)] | ||
| (doseq [value (gen/sample gen-example 1)] | ||
| (prn value)))) | ||
|
|
||
| (comment | ||
| (gen/sample (gen-schema example-spec) 1) | ||
| (gen/sample (gen-integer {:minimum 10 :maximum 300}) 1) | ||
| (gen/sample (gen-random-string 2 10)) | ||
| (gen/sample gen/string-alphanumeric 5) | ||
| (gen/sample (gen/such-that #(>= (count %) 2) gen/string-alphanumeric) 5) | ||
| (gen/sample (gen-string {:minLength 3 :maxLength 10})) | ||
| (gen/vector gen/char-alphanumeric 2 4) | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| (ns spec-faker.schema-validator | ||
| (:require [clojure.spec.alpha :as s])) | ||
|
|
||
| (s/def ::type #{"string" "integer" "boolean" "object"}) | ||
| (s/def ::format (s/nilable #{"email" "uuid"})) | ||
| (s/def ::minLength pos-int?) | ||
| (s/def ::maxLength pos-int?) | ||
| (s/def ::minimum number?) | ||
| (s/def ::maximum number?) | ||
|
|
||
| (s/def ::property | ||
| (s/keys :req-un [::type] | ||
| :opt-un [::format ::minLength ::maxLength ::minimum ::maximum])) | ||
|
|
||
| (s/def ::properties (s/map-of keyword? ::property)) | ||
| (s/def ::required (s/coll-of string? :kind vector?)) | ||
|
|
||
| (s/def ::openapi-schema | ||
| (s/keys :req-un [::type ::properties] | ||
| :opt-un [::required])) | ||
|
|
||
| (defn validate-schema [schema] | ||
| (s/valid? ::openapi-schema schema)) | ||
|
|
||
| (defn explain-validation-error [schema] | ||
| (s/explain-str ::openapi-schema schema)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| (ns spec-faker.test | ||
| (:require [clojure.test :refer :all] | ||
| [ring.mock.request :as mock] | ||
| [spec-faker.core :refer [app]]) | ||
| (:import (java.net URLEncoder))) | ||
|
|
||
| (deftest test-get-root | ||
| (testing "GET / without spec" | ||
| (let [response (app (mock/request :get "/"))] | ||
| (is (= 200 (:status response))) | ||
| (is (.contains (:body response) "Insert OpenAPI specification"))))) | ||
|
|
||
| (deftest test-get-with-spec | ||
| (testing "GET / with valid spec" | ||
| (let [spec "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}" | ||
| response (app (mock/request :get "/" {:spec spec})) | ||
| body (slurp (:body response))] | ||
| (is (= 200 (:status response))) | ||
| (is (.contains body "\"name\"")))) | ||
|
|
||
| (testing "GET / with invalid spec" | ||
| (let [spec "{\"type\":\"invalid\"}" | ||
| response (app (mock/request :get "/" {:spec spec})) | ||
| body (slurp (:body response))] | ||
| (is (= 400 (:status response))) | ||
| (is (.contains body "Invalid spec")))) | ||
|
|
||
| (testing "GET / with invalid JSON" | ||
| (let [spec "{\"type\":\"object\",\"properties\":{\"name\"}" | ||
| response (app (mock/request :get "/" {:spec spec})) | ||
| body (slurp (:body response))] | ||
| (is (= 400 (:status response))) | ||
| (is (.contains body "Invalid JSON"))))) | ||
|
|
||
| (deftest test-post-root | ||
| (testing "POST / with valid spec" | ||
| (let [spec "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}" | ||
| response (app (-> (mock/request :post "/") | ||
| (mock/content-type "application/x-www-form-urlencoded") | ||
| (mock/body (str "spec=" (URLEncoder/encode spec "UTF-8")))))] | ||
| (is (= 303 (:status response))) | ||
| (is (= (str "/?spec=" (URLEncoder/encode spec "UTF-8")) | ||
| (get-in response [:headers "Location"]))))) | ||
|
|
||
| (testing "POST / with invalid spec" | ||
| (let [spec "{\"type\":\"invalid\"}" | ||
| response (app (-> (mock/request :post "/") | ||
| (mock/content-type "application/x-www-form-urlencoded") | ||
| (mock/body (str "spec=" (URLEncoder/encode spec "UTF-8"))))) | ||
| body (slurp (:body response))] | ||
| (is (= 400 (:status response))) | ||
| (is (.contains body "Invalid Swagger spec")))) | ||
|
|
||
| (testing "POST / with invalid JSON" | ||
| (let [spec "{\"type\":\"object\",\"properties\":{\"name\"}" | ||
| response (app (-> (mock/request :post "/") | ||
| (mock/content-type "application/x-www-form-urlencoded") | ||
| (mock/body (str "spec=" (URLEncoder/encode spec "UTF-8"))))) | ||
| body (slurp (:body response))] | ||
| (is (= 400 (:status response))) | ||
| (is (.contains body "Invalid JSON"))))) | ||
|
|
||
|
|
||
| (deftest test-not-found | ||
| (testing "GET /unknown" | ||
| (let [response (app (mock/request :get "/unknown"))] | ||
| (is (= 404 (:status response))) | ||
| (is (= "Not Found" (:body response)))))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gen-class обычно в самом конце пишут. Или хотя бы require и import рядом располагают, как связанные по смыслу директивы