棱镜模式:删除意外的键
Prismatic schema: removing unanticipated keys
我的 API 正在从客户端接收一些 JSON 数据。
我想使用 Schema 对我收到的数据执行验证和强制转换,但有一个额外的要求:如果有任何 map key 未在 schema 中描述,忽略并删除它而不是失败验证(这是因为我的客户可能会向我发送一些 "garbage" 属性以及我关心的属性。我想对此宽容。)。
简而言之,我想在 validation/coercion.
之前使用我的模式对我的输入数据执行 "deep select-keys
"
我需要的示例:
(require '[schema.core :as sc])
(def MySchema {:a sc/Int
:b {:c sc/Str
(sc/optional-key :d) sc/Bool}
:e [{:f sc/Inst}]})
(sanitize-and-validate
MySchema
{:a 2
:b {:c "hello"
:$$garbage-key 32}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00" :garbage-key 42}]
:_garbage-key1 "woot"})
=> {:a 2
:b {:c "hello"}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
我还没有找到可靠的方法:
- 我似乎无法在 custom transformation 中执行此操作,因为助行器似乎不允许您使用钥匙。
- 我尝试手动遍历模式时运气不佳,因为很难以通用方式区分地图模式和标量模式;也很难说明模式可能具有的所有可能形状。
有没有明显的方法我没有看到?
谢谢!
For the special case of keywords, you can omit the required-key, like
{:foo s/Str :bar s/Keyword}. You can also provide specific optional
keys, and combine specific keys with generic schemas for the remaining
key-value mappings:
(def FancyMap
"If foo is present, it must map to a Keyword. Any number of additional
String-String mappings are allowed as well."
{(s/optional-key :foo) s/Keyword
s/Str s/Str})
(s/validate FancyMap {"a" "b"})
(s/validate FancyMap {:foo :f "c" "d" "e" "f"})
因此,除了您的特定键(可以是示例中的 s/optional-key
,或者似乎是您需要的 s/required-key
)之外,您还可以有额外的 "relaxed" 键,一些喜欢:
(def MySchema {:a sc/Int
:b {:c sc/Str
(sc/optional-key :d) sc/Bool
s/Any s/Any}
:e [{:f sc/Inst}]})
编辑:通过添加 :garbage
元数据并丢弃 walker 中的那些条目找到了 "hacky" 方法:
(def Myschema {:a s/Int
:b {:c s/Str
(s/optional-key :d) s/Bool
(with-meta s/Any {:garbage true}) s/Any}
:e [{:f s/Inst}]
(with-meta s/Any {:garbage true}) s/Any})
(defn garbage? [s]
(and (associative? s)
(:garbage (meta (:kspec s)))))
(defn discard-garbage [schema]
(s/start-walker
(fn [s]
(let [walk (s/walker s)]
(fn [x]
(let [result (walk x)]
(if (garbage? s)
(do (println "found garbage" x)
nil)
result)))))
schema))
((discard-garbage Myschema) data)
;=> :a 2, :b {:c "hello"}, :e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
这是另一种方法(代码如下):
- 定义自定义
Garbage
模式类型,以匹配您要删除的属性;如果你想删除所有未知属性,你可以使用 schema.core/Any
作为模式中的键(感谢 Colin Yates 告诉我这件事)。
- 作为强制步骤,'flag' 通过将所有值强制转换为垃圾类型的实例来删除所有值。
- 遍历数据结构以去除所有标志。
这样做的好处是可以对 Schema 的内部结构做很少的假设(在撰写本文时仍处于 alpha 阶段),并且至少有 2 个缺点:
- 假设数据是 Clojure 映射和序列的组合(在 JSON 输入的情况下不是真正的问题)
- 添加另一个数据结构遍历,从性能的角度来看这可能不是最优的。
(require '[schema.core :as s])
(require '[schema.coerce :as sco])
(require '[schema.utils :as scu])
(deftype ^:private GarbageType [])
(def ^:private garbage-const (GarbageType.))
(def Garbage "Garbage schema, use it to flag schema attributes to be removed by `cleaner`." GarbageType)
(defn garbage-flagging-matcher "schema.coerce matcher to detect and flag garbage values." [schema]
(cond (= schema Garbage) (constantly garbage-const)
:else identity))
(defn- garbage-flagger "Accepts a schema (supposedly that uses Garbage as a sub-schema), and returns a function that flags garbage values by coercing them to `garbage-const`"
[schema] (sco/coercer schema garbage-flagging-matcher))
(defn clean-garbage "Accepts a clojure data structures, and removes the values equal to `garbage-const."
[v]
(cond
(= garbage-const v) nil
(map? v) (->> v seq
(reduce (fn [m [k nv]]
(if (= garbage-const nv)
(dissoc m k)
(assoc m k (clean-garbage nv)))
) v))
(vector? v) (->> v (remove #(= % garbage-const)) (map clean-garbage) vec)
(sequential? v) (->> v (remove #(= % garbage-const)) (map clean-garbage) doall)
:else v
))
(defn cleaner "Accepts a Schema, which presumably uses Garbage to match illegal values, and returns a function that accepts a data structure (potentially an instance of the schema) and will remove its values that are not anticipated in the schema, e.g illegal map keys."
[schema]
(let [flag (garbage-flagger schema)]
(fn [data]
(-> data flag clean-garbage)
)))
;; Example
(def MySchema {:a s/Int
:b {:c s/Str
(s/optional-key :d) s/Bool
s/Any Garbage}
:e [{:f s/Inst
s/Any Garbage}]
s/Any Garbage})
((cleaner MySchema) {:a 1
:garbage-key "hello"
:b {:c "Hellow world"
:d false
42432424 23/2}
:e [{:f #inst "2015-07-23T15:49:33.073-00:00"
'a-garbage-key "remove me!!"
"another garbage key" :remove-me!!}
{:f #inst "2015-07-23T15:53:33.073-00:00"}]})
=> {:a 1
:b {:c "Hellow world"
:d false}
:e [{:f #inst "2015-07-23T15:49:33.073-00:00"}
{:f #inst "2015-07-23T15:53:33.073-00:00"}]}
第三个解决方案,归功于 abp:使用模式。coerce/coercer 与匹配器将从映射中删除未知键。
(require '[schema.core :as s])
(require '[schema.coerce :as coerce])
(require '[schema.utils :as utils])
(defn filter-schema-keys
[m schema-keys extra-keys-walker]
(reduce-kv (fn [m k v]
(if (or (contains? schema-keys k)
(and extra-keys-walker
(not (utils/error? (extra-keys-walker k)))))
m
(dissoc m k)))
m
m))
(defn map-filter-matcher
[s]
(when (or (instance? clojure.lang.PersistentArrayMap s)
(instance? clojure.lang.PersistentHashMap s))
(let [extra-keys-schema (#'s/find-extra-keys-schema s)
extra-keys-walker (when extra-keys-schema (s/walker extra-keys-schema))
explicit-keys (some->> (dissoc s extra-keys-schema)
keys
(mapv s/explicit-schema-key)
(into #{}))]
(when (or extra-keys-walker (seq explicit-keys))
(fn [x]
(if (map? x)
(filter-schema-keys x explicit-keys extra-keys-walker)
x))))))
这个 was described 是 Schema 的主要作者提出的最干净的解决方案,因为它不需要对模式本身进行任何更改即可工作。所以这可能是要走的路。
用法示例:
(def data {:a 2
:b {:c "hello"
:$$garbage-key 32}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00" :garbage-key 42}]
:_garbage-key1 "woot"})
((coerce/coercer MySchema map-filter-matcher) data)
;=> {:a 2, :b {:c "hello"}, :e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
有一个名为 "select-schema" 的架构工具。参见 https://github.com/metosin/schema-tools#select-schema
来自页面:
Select Schema
Filtering out illegal schema keys (using coercion):
(st/select-schema {:street "Keskustori 8"
:city "Tampere"
:description "Metosin HQ" ; disallowed-key
:country {:weather "-18" ; disallowed-key
:name "Finland"}}
Address)
; {:city "Tampere", :street "Keskustori 8", :country {:name "Finland"}}
Filtering out illegal schema map keys using coercion with additional
Json-coercion - in a single sweep:
(s/defschema Beer {:beer (s/enum :ipa :apa)})
(def ipa {:beer "ipa" :taste "good"})
(st/select-schema ipa Beer)
; clojure.lang.ExceptionInfo: Could not coerce value to schema: {:beer (not (#{:ipa :apa} "ipa"))}
; data: {:type :schema.core/error,
; :schema {:beer {:vs #{:ipa :apa}}},
; :value {:beer "ipa", :taste "good"},
; :error {:beer (not (#{:ipa :apa} "ipa"))}}
(require '[schema.coerce :as sc])
(st/select-schema ipa Beer sc/json-coercion-matcher)
; {:beer :ipa}
我的 API 正在从客户端接收一些 JSON 数据。
我想使用 Schema 对我收到的数据执行验证和强制转换,但有一个额外的要求:如果有任何 map key 未在 schema 中描述,忽略并删除它而不是失败验证(这是因为我的客户可能会向我发送一些 "garbage" 属性以及我关心的属性。我想对此宽容。)。
简而言之,我想在 validation/coercion.
之前使用我的模式对我的输入数据执行 "deepselect-keys
"
我需要的示例:
(require '[schema.core :as sc])
(def MySchema {:a sc/Int
:b {:c sc/Str
(sc/optional-key :d) sc/Bool}
:e [{:f sc/Inst}]})
(sanitize-and-validate
MySchema
{:a 2
:b {:c "hello"
:$$garbage-key 32}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00" :garbage-key 42}]
:_garbage-key1 "woot"})
=> {:a 2
:b {:c "hello"}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
我还没有找到可靠的方法:
- 我似乎无法在 custom transformation 中执行此操作,因为助行器似乎不允许您使用钥匙。
- 我尝试手动遍历模式时运气不佳,因为很难以通用方式区分地图模式和标量模式;也很难说明模式可能具有的所有可能形状。
有没有明显的方法我没有看到?
谢谢!
For the special case of keywords, you can omit the required-key, like {:foo s/Str :bar s/Keyword}. You can also provide specific optional keys, and combine specific keys with generic schemas for the remaining key-value mappings:
(def FancyMap "If foo is present, it must map to a Keyword. Any number of additional String-String mappings are allowed as well." {(s/optional-key :foo) s/Keyword s/Str s/Str}) (s/validate FancyMap {"a" "b"}) (s/validate FancyMap {:foo :f "c" "d" "e" "f"})
因此,除了您的特定键(可以是示例中的 s/optional-key
,或者似乎是您需要的 s/required-key
)之外,您还可以有额外的 "relaxed" 键,一些喜欢:
(def MySchema {:a sc/Int
:b {:c sc/Str
(sc/optional-key :d) sc/Bool
s/Any s/Any}
:e [{:f sc/Inst}]})
编辑:通过添加 :garbage
元数据并丢弃 walker 中的那些条目找到了 "hacky" 方法:
(def Myschema {:a s/Int
:b {:c s/Str
(s/optional-key :d) s/Bool
(with-meta s/Any {:garbage true}) s/Any}
:e [{:f s/Inst}]
(with-meta s/Any {:garbage true}) s/Any})
(defn garbage? [s]
(and (associative? s)
(:garbage (meta (:kspec s)))))
(defn discard-garbage [schema]
(s/start-walker
(fn [s]
(let [walk (s/walker s)]
(fn [x]
(let [result (walk x)]
(if (garbage? s)
(do (println "found garbage" x)
nil)
result)))))
schema))
((discard-garbage Myschema) data)
;=> :a 2, :b {:c "hello"}, :e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
这是另一种方法(代码如下):
- 定义自定义
Garbage
模式类型,以匹配您要删除的属性;如果你想删除所有未知属性,你可以使用schema.core/Any
作为模式中的键(感谢 Colin Yates 告诉我这件事)。 - 作为强制步骤,'flag' 通过将所有值强制转换为垃圾类型的实例来删除所有值。
- 遍历数据结构以去除所有标志。
这样做的好处是可以对 Schema 的内部结构做很少的假设(在撰写本文时仍处于 alpha 阶段),并且至少有 2 个缺点:
- 假设数据是 Clojure 映射和序列的组合(在 JSON 输入的情况下不是真正的问题)
- 添加另一个数据结构遍历,从性能的角度来看这可能不是最优的。
(require '[schema.core :as s])
(require '[schema.coerce :as sco])
(require '[schema.utils :as scu])
(deftype ^:private GarbageType [])
(def ^:private garbage-const (GarbageType.))
(def Garbage "Garbage schema, use it to flag schema attributes to be removed by `cleaner`." GarbageType)
(defn garbage-flagging-matcher "schema.coerce matcher to detect and flag garbage values." [schema]
(cond (= schema Garbage) (constantly garbage-const)
:else identity))
(defn- garbage-flagger "Accepts a schema (supposedly that uses Garbage as a sub-schema), and returns a function that flags garbage values by coercing them to `garbage-const`"
[schema] (sco/coercer schema garbage-flagging-matcher))
(defn clean-garbage "Accepts a clojure data structures, and removes the values equal to `garbage-const."
[v]
(cond
(= garbage-const v) nil
(map? v) (->> v seq
(reduce (fn [m [k nv]]
(if (= garbage-const nv)
(dissoc m k)
(assoc m k (clean-garbage nv)))
) v))
(vector? v) (->> v (remove #(= % garbage-const)) (map clean-garbage) vec)
(sequential? v) (->> v (remove #(= % garbage-const)) (map clean-garbage) doall)
:else v
))
(defn cleaner "Accepts a Schema, which presumably uses Garbage to match illegal values, and returns a function that accepts a data structure (potentially an instance of the schema) and will remove its values that are not anticipated in the schema, e.g illegal map keys."
[schema]
(let [flag (garbage-flagger schema)]
(fn [data]
(-> data flag clean-garbage)
)))
;; Example
(def MySchema {:a s/Int
:b {:c s/Str
(s/optional-key :d) s/Bool
s/Any Garbage}
:e [{:f s/Inst
s/Any Garbage}]
s/Any Garbage})
((cleaner MySchema) {:a 1
:garbage-key "hello"
:b {:c "Hellow world"
:d false
42432424 23/2}
:e [{:f #inst "2015-07-23T15:49:33.073-00:00"
'a-garbage-key "remove me!!"
"another garbage key" :remove-me!!}
{:f #inst "2015-07-23T15:53:33.073-00:00"}]})
=> {:a 1
:b {:c "Hellow world"
:d false}
:e [{:f #inst "2015-07-23T15:49:33.073-00:00"}
{:f #inst "2015-07-23T15:53:33.073-00:00"}]}
第三个解决方案,归功于 abp:使用模式。coerce/coercer 与匹配器将从映射中删除未知键。
(require '[schema.core :as s])
(require '[schema.coerce :as coerce])
(require '[schema.utils :as utils])
(defn filter-schema-keys
[m schema-keys extra-keys-walker]
(reduce-kv (fn [m k v]
(if (or (contains? schema-keys k)
(and extra-keys-walker
(not (utils/error? (extra-keys-walker k)))))
m
(dissoc m k)))
m
m))
(defn map-filter-matcher
[s]
(when (or (instance? clojure.lang.PersistentArrayMap s)
(instance? clojure.lang.PersistentHashMap s))
(let [extra-keys-schema (#'s/find-extra-keys-schema s)
extra-keys-walker (when extra-keys-schema (s/walker extra-keys-schema))
explicit-keys (some->> (dissoc s extra-keys-schema)
keys
(mapv s/explicit-schema-key)
(into #{}))]
(when (or extra-keys-walker (seq explicit-keys))
(fn [x]
(if (map? x)
(filter-schema-keys x explicit-keys extra-keys-walker)
x))))))
这个 was described 是 Schema 的主要作者提出的最干净的解决方案,因为它不需要对模式本身进行任何更改即可工作。所以这可能是要走的路。
用法示例:
(def data {:a 2
:b {:c "hello"
:$$garbage-key 32}
:e [{:f #inst "2015-07-23T12:29:51.822-00:00" :garbage-key 42}]
:_garbage-key1 "woot"})
((coerce/coercer MySchema map-filter-matcher) data)
;=> {:a 2, :b {:c "hello"}, :e [{:f #inst "2015-07-23T12:29:51.822-00:00"}]}
有一个名为 "select-schema" 的架构工具。参见 https://github.com/metosin/schema-tools#select-schema
来自页面:
Select Schema
Filtering out illegal schema keys (using coercion):
(st/select-schema {:street "Keskustori 8" :city "Tampere" :description "Metosin HQ" ; disallowed-key :country {:weather "-18" ; disallowed-key :name "Finland"}} Address) ; {:city "Tampere", :street "Keskustori 8", :country {:name "Finland"}}
Filtering out illegal schema map keys using coercion with additional Json-coercion - in a single sweep:
(s/defschema Beer {:beer (s/enum :ipa :apa)}) (def ipa {:beer "ipa" :taste "good"}) (st/select-schema ipa Beer) ; clojure.lang.ExceptionInfo: Could not coerce value to schema: {:beer (not (#{:ipa :apa} "ipa"))} ; data: {:type :schema.core/error, ; :schema {:beer {:vs #{:ipa :apa}}}, ; :value {:beer "ipa", :taste "good"}, ; :error {:beer (not (#{:ipa :apa} "ipa"))}} (require '[schema.coerce :as sc]) (st/select-schema ipa Beer sc/json-coercion-matcher) ; {:beer :ipa}