嵌套级别之间具有相互依赖值的地图规范?

问题描述

我正在尝试为 GraphQL 架构语法的一部分定义规范。下面是从 API 返回的字段类型的样子(注意 :ofType 可以无限嵌套):

{:kind "NON_NULL",:name nil,:ofType {:kind "LIST",:ofType {:kind "NON_NULL",:ofType {:kind "OBJECT",:name "Comment"}}}}

目前我有一个这样的规范来表示这个结构:

(spec/def ::kind #{"NON_NULL" "LIST" "SCALAR" "OBJECT"})
(spec/def ::name (spec/nilable string?))
(spec/def ::ofType (spec/or :terminal nil?
                            :type ::type))

(spec/def ::type
  (spec/keys :req-un [::name ::kind ::ofType]))

这是一个不错的解决方案,但我还没有弄清楚如何捕获一些不变量:

  1. :name 在除最深(终端)级别之外的所有级别都必须为零。
  2. :kind 在终端级别只能等于 SCALAROBJECT
  3. :kind 必须在终端级别等于 SCALAROBJECT
  4. :kind 不能在两个连续级别中等于 NON_NULL

是否可以在规范中捕获这些规则?或者,如果没有,是否可以编写一个遵守这些规则的自定义生成器?

更新 - 发电机解决方

我能够为此目的构建一个生成器。有关如何直接指定此内容,请参阅下面的 Rulle 回答。


(spec/def ::kind #{"NON_NULL" "LIST" "SCALAR" "OBJECT"})
(spec/def ::name (spec/nilable string?))
(spec/def ::ofType (spec/or :terminal nil?
                            :type ::type))

(spec/def ::type
  (spec/keys :req-un [::name ::kind ::ofType]))

(spec/def ::terminal-kind #{"SCALAR" "OBJECT"})
(spec/def ::terminal-name string?)

(def terminal-gen
  "Returns a generator for a terminal field type.
  Terminal field types are either of :kind 'OBJECT' or 'SCALAR',have an :ofType of nil,and a non-nil :name."
  (gen/bind
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
      (gen/hash-map
        :name (spec/gen #{name})
        :kind (spec/gen #{kind})
        :ofType (gen/return nil)))))

(defn build-type-gen
  "Returns a generator which constructs a field type data structure.

  An example of a field type:

   { :name nil,:kind 'NON_NULL',:ofType {:name nil,<-- a 'modifier layer',these can be infinitely nested
              :kind 'LIST',:kind 'LIST',:ofType {:name 'M17Pyn0zClVD',:kind 'OBJECT',:ofType nil}}}}} <-- Terminal field type

  This function works by creating a terminal type generator,then 'wrapping' it with layer generators (NON NULL and LIST)
  until it's a given depth. The following constraints are ensured through the process:

  1. :name must be nil at all levels except the deepest (terminal) level.
  2. :kind can only equal SCALAR or OBJECT at the terminal level.
  3. :kind must equal either SCALAR or OBJECT at the terminal level.
  4. :kind cannot equal NON_NULL in two consecutive levels."
  ([max-depth] (if (= max-depth 1) terminal-gen
                                   (build-type-gen max-depth 0 terminal-gen)))
  ([max-depth curr-depth inner-gen]
   (if (< curr-depth max-depth)
     (recur max-depth
            (inc curr-depth)
            (gen/bind inner-gen
                      (fn [inner-gen]
                        (if (= "NON_NULL" (:kind inner-gen))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen #{"LIST"}) ; two NON_NULLs cannot be child-parent
                            :ofType (spec/gen #{inner-gen}))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen #{"NON_NULL" "LIST"})
                            :ofType (spec/gen #{inner-gen}))))))
     inner-gen)))

(def type-gen (gen/bind (spec/gen (spec/int-in 1 5)) build-type-gen))

示例:

(gen/generate type-gen)
=>
{:name nil,:kind "LIST",:kind "NON_NULL",:ofType {:name "KmgbOsy",:kind "SCALAR",:ofType nil}}}}}

解决方法

你可以把它分成ofType内部外部变体,像这样:

(spec/def :inner/name nil?)
(spec/def :inner/kind #{"NON_NULL" "LIST" "SCALAR" "OBJECT"})
(spec/def :inner/ofType (spec/or :inner ::inner
                                 :outer ::outer))
(spec/def ::inner (spec/keys :req-un [:inner/name :inner/kind :inner/ofType]))

(spec/def :outer/name (spec/nilable string?))
(spec/def :outer/kind #{"SCALAR" "OBJECT"})
(spec/def :outer/ofType nil?)
(spec/def ::outer (spec/keys :req-un [:outer/name :outer/kind] :opt-un [:outer/ofType]))

(spec/def ::ofType (spec/or :terminal nil?
                            :inner ::inner
                            :outer ::outer))

最后一个条件,即两个 NON_NULL 不能相互跟随,可以单独处理。请注意,此函数将在稍后在规范中使用时接收 conformed 值:

(defn kinds-ok? [of-type]
  (->> (second of-type)
       (iterate (comp second :ofType))
       (map :kind)
       (take-while some?)
       (partition 2 1)
       (some #{["NON_NULL" "NON_NULL"]})
       not))

然后我们用这个额外的条件创建一个顶级规范:

(spec/def ::top (spec/and ::ofType kinds-ok?))

kinds-ok? 将接收符合标准的值而不是原始值这一事实是规范的一个有点令人惊讶甚至令人讨厌的方面。但这就是它的设计方式。我不知道如何让它获得原始价值:如果有人知道,请随时提出建议。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...