Призматическая схема: удаление непредвиденных ключей

Мой API получает некоторые данные JSON от клиента.

Я хотел бы использовать схему для выполнения проверки и принуждения к данным, которые я получаю, но с одним дополнительным требованием: если есть какой-либо ключ карты, который не описан в схеме, проигнорируйте и удалите его вместо того, чтобы не пройти проверку (это потому, что мой клиент может прислать мне несколько «мусорных» свойств вместе с теми, которые мне небезразличны. Я хочу быть терпимым к этому.).

Итак, в двух словах, я хотел бы выполнить «глубокое 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. Я не могу сделать это в пользовательском преобразовании, потому что это кажется ходок не дает вам доступа к ключам.
  2. Мне не удалось пройтись по схеме вручную, потому что трудно различать схемы карты и скалярные схемы общим способом; также трудно учесть все возможные формы, которые может иметь схема.

Есть ли очевидный способ, которого я не вижу?

Спасибо!


person Valentin Waeselynck    schedule 23.07.2015    source источник


Ответы (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))))))

Это было описано как самое чистое решение основным автором 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"}]}
person Valentin Waeselynck    schedule 23.07.2015
comment
Я думаю, это лучшее решение, только что нашел одну проблему: я попробовал, но не сразу сработало с данными из вопроса: карта с ключом :garbage превращается в nil, и это недействительно против :e [{:f sc/Inst}] . Объединение карт с :f и :garbage-key работает. Поэтому, если ваши данные имеют ту же форму, что и в вопросе, они не будут работать как есть. В противном случае я опубликовал редактирование этого ответа с объединенными картами, показывающими, как используется сопоставитель. - person nberger; 24.07.2015
comment
Вы конечно правы, я поправил вопрос. Спасибо! - person Valentin Waeselynck; 24.07.2015
comment
Примечание. В более поздней версии Schema замените s/walker на s/checker. - person siltalau; 01.07.2016

Из 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}]})

EDIT: нашел «хакерский» способ сделать это, добавив метаданные :garbage и удалив эти записи в ходунке:

(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"}]}
person nberger    schedule 23.07.2015
comment
Спасибо! Дело в том, что мне нужно не только игнорировать дополнительные ключи, но и удалить их (я знаю, это странно). - person Valentin Waeselynck; 23.07.2015
comment
Извините, я пропустил эту часть :/. Я играю с ходунком, но обнаружил ту же проблему, что и вы, не так просто отличить схему карты - person nberger; 23.07.2015
comment
Колин Йейтс помог мне найти решение. Просто нужно некоторое время, чтобы понять это. - person Valentin Waeselynck; 23.07.2015
comment
Добавил решение с ходунком. Он идентифицирует записи, используя метаданные :garbage. Вы также можете обойтись без метаданных :garbage, заменив garbage? чем-то вроде (and (associative? s) (= [:kspec :val-schema] (keys s)) (s/Any (:kspec s))). Дело в том, что я не смог найти другого способа идентифицировать schema.core.MapEntry, который в этих случаях является типом s. - person nberger; 23.07.2015
comment
Хм, интересно, не знал об этом: kspec. У меня было что-то немного другое, используя приведение к пользовательскому типу мусора. Мне это кажется довольно солидным, почему вы называете это хакерским? - person Valentin Waeselynck; 23.07.2015
comment
Я назвал это хакерским, потому что он использует метаданные в схеме вместо правильной схемы, а также потому, что это зависит от внутренней реализации s/MapEntry. Это работает, но может сломаться в будущей версии схемы без предварительного уведомления. - person nberger; 23.07.2015
comment
Давайте продолжим обсуждение в чате. - person Valentin Waeselynck; 23.07.2015

Для этого существует инструмент схемы, который называется "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-приведением — за один проход:

(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}
person Allen    schedule 27.09.2017

Вот еще один подход (код ниже):

  1. Определите пользовательские типы схемы Garbage, которые будут сопоставляться со свойствами, которые вы хотите удалить; если вы хотите удалить все неизвестные свойства, вы можете использовать schema.core/Any в качестве ключа в своей схеме (спасибо Колину Йейтсу за то, что он рассказал мне об этом).
  2. В качестве шага принуждения «отметьте» все значения, которые необходимо удалить, приведя их к экземпляру типа мусора.
  3. Пройдитесь по структуре данных, чтобы снять все флаги.

Это имеет то преимущество, что делает небольшие предположения о внутренностях схемы (все еще в альфа-версии на момент написания), и по крайней мере 2 недостатка:

  1. Предполагается, что данные представляют собой комбинацию карт и последовательностей Clojure (на самом деле это не проблема в случае ввода JSON).
  2. Добавляет еще один обход структуры данных, что может быть неоптимальным с точки зрения производительности.

(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"}]}
person Valentin Waeselynck    schedule 23.07.2015