棱彩模式:删除意外的密钥

时间:2015-07-23 12:37:33

标签: validation clojure input-sanitization plumatic-schema

我的API正在从客户端接收一些JSON数据。

我想使用Schema对我收到的数据执行验证和强制,但还有一个额外的要求:如果有任何未在架构中描述的映射键,请忽略并删除它而不是验证失败(这是因为我的客户可能会向我发送一些“垃圾”属性以及我关心的属性。我希望能够容忍这一点。)。

简而言之,我想在验证/强制之前使用我的架构对我的输入数据执行“深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"}]}

我还没有找到一种可行的方法:

  1. 我似乎无法在custom transformation中执行此操作,因为看起来助行器无法让您访问密钥。
  2. 我没有任何运气试图手动遍历模式,因为很难以通用的方式区分地图模式和标量模式;也难以解释模式可能具有的所有可能形状。
  3. 有没有一种我没有看到的明显方式?

    谢谢!

4 个答案:

答案 0 :(得分:4)

第三个解决方案,归功于abp:使用schema.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"}]}

答案 1 :(得分:1)

来自Schema README

  

对于关键字的特殊情况,您可以省略所需的键,例如   {:foo s / Str:bar s / Keyword}。您还可以提供特定的可选项   键,并将特定键与剩余的通用模式组合在一起   键值映射:

(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似乎是您的需要)之外,您还可以享受额外的"轻松"键,类似于:

(def MySchema {:a sc/Int
               :b {:c sc/Str
                   (sc/optional-key :d) sc/Bool
                   s/Any s/Any}
               :e [{:f sc/Inst}]})

编辑:找到一个" hacky"通过添加:garbage元数据并在walker中丢弃这些条目来实现此目的的方法:

(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"}]}

答案 2 :(得分:0)

这是另一种方法(下面的代码):

  1. 定义自定义Garbage架构类型,以与要删除的属性进行匹配;如果您希望删除所有未知属性,可以使用schema.core/Any作为模式中的关键字(归功于Colin Yates告诉我这一点)。
  2. 作为一个强制步骤,'标记'通过将它们强制转换为垃圾类型的实例来删除所有值。
  3. 遍历数据结构以去除所有标记。
  4. 这样做的好处是对Schema的内部结构做了很少的假设(在撰写本文时仍处于alpha状态),并且至少有两个缺点:

    1. 假设数据是Clojure地图和序列的组合(在JSON输入的情况下不是真正的问题)
    2. 添加数据结构的另一次遍历,从性能角度来看可能不是最佳的。
    3. (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"}]}
      

答案 3 :(得分:0)

这个名为" select-schema"的架构工具。见https://github.com/metosin/schema-tools#select-schema

从页面:

  

选择架构

     

过滤掉非法架构密钥(使用强制):

(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"}}
     

使用强制附加功能过滤掉非法模式映射键   Json-cercion - 一次扫描:

(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}