嵌套级别之间具有相互依赖值的地图规范?
Spec for map with interdependent values between nested levels?
我正在尝试为 GraphQL 模式语法的一部分定义规范。这是从 API 返回的字段类型的样子(请注意:ofType 可以无限嵌套):
{:kind "NON_NULL",
:name nil,
:ofType {:kind "LIST",
:name nil,
:ofType {:kind "NON_NULL",
:name nil,
: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]))
这是一个不错的解决方案,但有一些不变量我还没有弄清楚如何捕获:
:name
除最深(终端)级别外的所有级别都必须为零。
:kind
只能在终端级别等于 SCALAR
或 OBJECT
。
:kind
必须 在终端级别等于 SCALAR
或 OBJECT
。
: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',
:ofType {:name nil,
: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",
:ofType {:name nil,
:kind "LIST",
:ofType {:name nil,
:kind "NON_NULL",
:ofType {:name nil, :kind "LIST", :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?
将收到一致的值而不是原始值这一事实在规范方面有些令人惊讶,甚至令人讨厌。但这就是它的设计方式。我不确定如何让它接收原始值:如果有人知道,请随时提出建议。
我正在尝试为 GraphQL 模式语法的一部分定义规范。这是从 API 返回的字段类型的样子(请注意:ofType 可以无限嵌套):
{:kind "NON_NULL",
:name nil,
:ofType {:kind "LIST",
:name nil,
:ofType {:kind "NON_NULL",
:name nil,
: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]))
这是一个不错的解决方案,但有一些不变量我还没有弄清楚如何捕获:
:name
除最深(终端)级别外的所有级别都必须为零。:kind
只能在终端级别等于SCALAR
或OBJECT
。:kind
必须 在终端级别等于SCALAR
或OBJECT
。: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',
:ofType {:name nil,
: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",
:ofType {:name nil,
:kind "LIST",
:ofType {:name nil,
:kind "NON_NULL",
:ofType {:name nil, :kind "LIST", :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?
将收到一致的值而不是原始值这一事实在规范方面有些令人惊讶,甚至令人讨厌。但这就是它的设计方式。我不确定如何让它接收原始值:如果有人知道,请随时提出建议。