graphql 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/define/assign_argument.rb +3 -5
  3. data/lib/graphql/field.rb +1 -3
  4. data/lib/graphql/input_object_type.rb +4 -1
  5. data/lib/graphql/language.rb +1 -0
  6. data/lib/graphql/language/generation.rb +94 -0
  7. data/lib/graphql/language/lexer.rb +229 -201
  8. data/lib/graphql/language/lexer.rl +24 -7
  9. data/lib/graphql/language/nodes.rb +4 -0
  10. data/lib/graphql/language/parser.rb +195 -187
  11. data/lib/graphql/language/parser.y +11 -4
  12. data/lib/graphql/object_type.rb +21 -0
  13. data/lib/graphql/query.rb +8 -3
  14. data/lib/graphql/query/serial_execution/operation_resolution.rb +2 -2
  15. data/lib/graphql/query/serial_execution/value_resolution.rb +5 -0
  16. data/lib/graphql/schema.rb +11 -19
  17. data/lib/graphql/schema/invalid_type_error.rb +6 -0
  18. data/lib/graphql/schema/reduce_types.rb +68 -0
  19. data/lib/graphql/schema/type_expression.rb +6 -16
  20. data/lib/graphql/schema/type_map.rb +1 -5
  21. data/lib/graphql/schema/validation.rb +164 -0
  22. data/lib/graphql/version.rb +1 -1
  23. data/readme.md +3 -4
  24. data/spec/graphql/argument_spec.rb +20 -0
  25. data/spec/graphql/field_spec.rb +16 -0
  26. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  27. data/spec/graphql/language/generation_spec.rb +42 -0
  28. data/spec/graphql/language/parser_spec.rb +136 -26
  29. data/spec/graphql/query/serial_execution/value_resolution_spec.rb +23 -0
  30. data/spec/graphql/query_spec.rb +51 -0
  31. data/spec/graphql/schema/{type_reducer_spec.rb → reduce_types_spec.rb} +16 -14
  32. data/spec/graphql/schema/type_expression_spec.rb +4 -4
  33. data/spec/graphql/schema/validation_spec.rb +219 -0
  34. data/spec/support/dairy_app.rb +3 -0
  35. metadata +14 -13
  36. data/lib/graphql/schema/each_item_validator.rb +0 -21
  37. data/lib/graphql/schema/field_validator.rb +0 -17
  38. data/lib/graphql/schema/implementation_validator.rb +0 -31
  39. data/lib/graphql/schema/type_reducer.rb +0 -79
  40. data/lib/graphql/schema/type_validator.rb +0 -58
  41. data/spec/graphql/schema/field_validator_spec.rb +0 -21
  42. data/spec/graphql/schema/type_validator_spec.rb +0 -81
@@ -108,11 +108,14 @@ rule
108
108
  }
109
109
 
110
110
  name:
111
+ name_without_on
112
+ | ON
113
+
114
+ name_without_on:
111
115
  IDENTIFIER
112
116
  | FRAGMENT
113
117
  | TRUE
114
118
  | FALSE
115
- | ON
116
119
 
117
120
  arguments_opt:
118
121
  /* none */ { return [] }
@@ -171,7 +174,7 @@ rule
171
174
  directive: DIR_SIGN name arguments_opt { return make_node(:Directive, name: val[1], arguments: val[2], position_source: val[0]) }
172
175
 
173
176
  fragment_spread:
174
- ELLIPSIS name directives_list_opt { return make_node(:FragmentSpread, name: val[1], directives: val[2], position_source: val[0]) }
177
+ ELLIPSIS name_without_on directives_list_opt { return make_node(:FragmentSpread, name: val[1], directives: val[2], position_source: val[0]) }
175
178
 
176
179
  inline_fragment:
177
180
  ELLIPSIS ON name directives_list_opt selection_set {
@@ -192,7 +195,7 @@ rule
192
195
  }
193
196
 
194
197
  fragment_definition:
195
- FRAGMENT name ON name directives_list_opt selection_set {
198
+ FRAGMENT name ON name_without_on directives_list_opt selection_set {
196
199
  return make_node(:FragmentDefinition, {
197
200
  name: val[1],
198
201
  type: val[3],
@@ -248,7 +251,11 @@ def on_error(parser_token_id, lexer_token, vstack)
248
251
  raise GraphQL::ParseError.new("Parse Error on unknown token: {token_id: #{parser_token_id}, lexer_token: #{lexer_token}} from #{@query_string}", nil, nil, @query_string)
249
252
  else
250
253
  line, col = lexer_token.line_and_column
251
- raise GraphQL::ParseError.new("Parse error on #{lexer_token.to_s.inspect} (#{parser_token_name}) at [#{line}, #{col}]", line, col, @query_string)
254
+ if lexer_token.name == :BAD_UNICODE_ESCAPE
255
+ raise GraphQL::ParseError.new("Parse error on bad Unicode escape sequence: #{lexer_token.to_s.inspect} (#{parser_token_name}) at [#{line}, #{col}]", line, col, @query_string)
256
+ else
257
+ raise GraphQL::ParseError.new("Parse error on #{lexer_token.to_s.inspect} (#{parser_token_name}) at [#{line}, #{col}]", line, col, @query_string)
258
+ end
252
259
  end
253
260
  end
254
261
  end
@@ -59,5 +59,26 @@ module GraphQL
59
59
  memo.merge!(iface.fields)
60
60
  end
61
61
  end
62
+
63
+
64
+ # Error raised when the value provided for a field can't be resolved to one of the possible types
65
+ # for the field.
66
+ class UnresolvedTypeError < GraphQL::Error
67
+ attr_reader :field_name, :field_type, :parent_type
68
+
69
+ def initialize(field_name, field_type, parent_type)
70
+ @field_name = field_name
71
+ @field_type = field_type
72
+ @parent_type = parent_type
73
+ super(exception_message)
74
+ end
75
+
76
+ private
77
+
78
+ def exception_message
79
+ "The value returned for field #{field_name} on #{parent_type} could not be resolved "\
80
+ "to one of the possible types for #{field_type}."
81
+ end
82
+ end
62
83
  end
63
84
  end
data/lib/graphql/query.rb CHANGED
@@ -8,7 +8,7 @@ module GraphQL
8
8
  end
9
9
  end
10
10
 
11
- attr_reader :schema, :document, :context, :fragments, :operations, :debug, :max_depth
11
+ attr_reader :schema, :document, :context, :fragments, :operations, :debug, :root_value, :max_depth
12
12
 
13
13
  # Prepare query `query_string` on `schema`
14
14
  # @param schema [GraphQL::Schema]
@@ -18,17 +18,22 @@ module GraphQL
18
18
  # @param debug [Boolean] if true, errors are raised, if false, errors are put in the `errors` key
19
19
  # @param validate [Boolean] if true, `query_string` will be validated with {StaticValidation::Validator}
20
20
  # @param operation_name [String] if the query string contains many operations, this is the one which should be executed
21
- def initialize(schema, query_string, context: nil, variables: {}, debug: false, validate: true, operation_name: nil, max_depth: nil)
21
+ # @param root_value [Object] the object used to resolve fields on the root type
22
+ def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, debug: false, validate: true, operation_name: nil, root_value: nil, max_depth: nil)
23
+ fail ArgumentError, "a query string or document is required" unless query_string || document
24
+
22
25
  @schema = schema
23
26
  @debug = debug
24
27
  @max_depth = max_depth || schema.max_depth
25
28
  @context = Context.new(query: self, values: context)
29
+ @root_value = root_value
26
30
  @validate = validate
27
31
  @operation_name = operation_name
28
32
  @fragments = {}
29
33
  @operations = {}
30
34
  @provided_variables = variables
31
- @document = GraphQL.parse(query_string)
35
+
36
+ @document = document || GraphQL.parse(query_string)
32
37
  @document.definitions.each do |part|
33
38
  if part.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
34
39
  @fragments[part.name] = part
@@ -2,7 +2,7 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class OperationResolution
5
- attr_reader :query, :target, :ast_operation_definition, :execution_context
5
+ attr_reader :target, :ast_operation_definition, :execution_context
6
6
 
7
7
  def initialize(ast_operation_definition, target, execution_context)
8
8
  @ast_operation_definition = ast_operation_definition
@@ -13,7 +13,7 @@ module GraphQL
13
13
  def result
14
14
  selections = ast_operation_definition.selections
15
15
  execution_context.strategy.selection_resolution.new(
16
- nil,
16
+ execution_context.query.root_value,
17
17
  target,
18
18
  selections,
19
19
  execution_context
@@ -55,6 +55,11 @@ module GraphQL
55
55
  class HasPossibleTypeResolution < BaseResolution
56
56
  def non_null_result
57
57
  resolved_type = field_type.resolve_type(value, execution_context)
58
+
59
+ unless resolved_type.is_a?(GraphQL::ObjectType)
60
+ raise GraphQL::ObjectType::UnresolvedTypeError.new(ast_field.name, field_type, parent_type)
61
+ end
62
+
58
63
  strategy_class = get_strategy_for_kind(resolved_type.kind)
59
64
  inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, ast_field, execution_context)
60
65
  inner_strategy.result
@@ -1,3 +1,12 @@
1
+ require "graphql/schema/invalid_type_error"
2
+ require "graphql/schema/middleware_chain"
3
+ require "graphql/schema/rescue_middleware"
4
+ require "graphql/schema/possible_types"
5
+ require "graphql/schema/reduce_types"
6
+ require "graphql/schema/type_expression"
7
+ require "graphql/schema/type_map"
8
+ require "graphql/schema/validation"
9
+
1
10
  module GraphQL
2
11
  # A GraphQL schema which may be queried with {GraphQL::Query}.
3
12
  class Schema
@@ -43,7 +52,7 @@ module GraphQL
43
52
  def types
44
53
  @types ||= begin
45
54
  all_types = @orphan_types + [query, mutation, GraphQL::Introspection::SchemaType]
46
- TypeReducer.find_all(all_types.compact)
55
+ GraphQL::Schema::ReduceTypes.reduce(all_types.compact)
47
56
  end
48
57
  end
49
58
 
@@ -73,7 +82,7 @@ module GraphQL
73
82
  end
74
83
 
75
84
  def type_from_ast(ast_node)
76
- GraphQL::Schema::TypeExpression.new(self, ast_node).type
85
+ GraphQL::Schema::TypeExpression.build_type(self, ast_node)
77
86
  end
78
87
 
79
88
  # @param type_defn [GraphQL::InterfaceType, GraphQL::UnionType] the type whose members you want to retrieve
@@ -82,22 +91,5 @@ module GraphQL
82
91
  @interface_possible_types ||= GraphQL::Schema::PossibleTypes.new(self)
83
92
  @interface_possible_types.possible_types(type_defn)
84
93
  end
85
-
86
- class InvalidTypeError < GraphQL::Error
87
- def initialize(type, name)
88
- super("#{name} has an invalid type: must be an instance of GraphQL::BaseType, not #{type.class.inspect} (#{type.inspect})")
89
- end
90
- end
91
94
  end
92
95
  end
93
-
94
- require "graphql/schema/each_item_validator"
95
- require "graphql/schema/field_validator"
96
- require "graphql/schema/implementation_validator"
97
- require "graphql/schema/middleware_chain"
98
- require "graphql/schema/rescue_middleware"
99
- require "graphql/schema/possible_types"
100
- require "graphql/schema/type_expression"
101
- require "graphql/schema/type_reducer"
102
- require "graphql/schema/type_map"
103
- require "graphql/schema/type_validator"
@@ -0,0 +1,6 @@
1
+ module GraphQL
2
+ class Schema
3
+ class InvalidTypeError < GraphQL::Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,68 @@
1
+ module GraphQL
2
+ class Schema
3
+ module ReduceTypes
4
+ # @param types [Array<GraphQL::BaseType>] members of a schema to crawl for all member types
5
+ # @return [GraphQL::Schema::TypeMap] `{name => Type}` pairs derived from `types`
6
+ def self.reduce(types)
7
+ type_map = GraphQL::Schema::TypeMap.new
8
+ types.each do |type|
9
+ reduce_type(type, type_map, type.name)
10
+ end
11
+ type_map
12
+ end
13
+
14
+ private
15
+
16
+ # Based on `type`, add members to `type_hash`.
17
+ # If `type` has already been visited, just return the `type_hash` as-is
18
+ def self.reduce_type(type, type_hash, context_description)
19
+ if !type.is_a?(GraphQL::BaseType)
20
+ message = "#{context_description} has an invalid type: must be an instance of GraphQL::BaseType, not #{type.class.inspect} (#{type.inspect})"
21
+ raise GraphQL::Schema::InvalidTypeError.new(message)
22
+ end
23
+
24
+ type = type.unwrap
25
+
26
+ # Don't re-visit a type
27
+ if !type_hash.fetch(type.name, nil).equal?(type)
28
+ validate_type(type, context_description)
29
+ type_hash[type.name] = type
30
+ crawl_type(type, type_hash, context_description)
31
+ end
32
+ end
33
+
34
+ def self.crawl_type(type, type_hash, context_description)
35
+ if type.kind.fields?
36
+ type.all_fields.each do |field|
37
+ reduce_type(field.type, type_hash, "Field #{type.name}.#{field.name}")
38
+ field.arguments.each do |name, argument|
39
+ reduce_type(argument.type, type_hash, "Argument #{name} on #{type.name}.#{field.name}")
40
+ end
41
+ end
42
+ end
43
+ if type.kind.object?
44
+ type.interfaces.each do |interface|
45
+ reduce_type(interface, type_hash, "Interface on #{type.name}")
46
+ end
47
+ end
48
+ if type.kind.union?
49
+ type.possible_types.each do |possible_type|
50
+ reduce_type(possible_type, type_hash, "Possible type for #{type.name}")
51
+ end
52
+ end
53
+ if type.kind.input_object?
54
+ type.arguments.each do |argument_name, argument|
55
+ reduce_type(argument.type, type_hash, "Input field #{type.name}.#{argument_name}")
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.validate_type(type, context_description)
61
+ error_message = GraphQL::Schema::Validation.validate(type)
62
+ if error_message
63
+ raise GraphQL::Schema::InvalidTypeError.new("#{context_description} is invalid: #{error_message}")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,25 +1,15 @@
1
1
  module GraphQL
2
2
  class Schema
3
- class TypeExpression
4
- def initialize(schema, ast_node)
5
- @schema = schema
6
- @ast_node = ast_node
7
- end
8
-
9
- def type
10
- @type ||= build_type(@schema, @ast_node)
11
- end
12
-
13
- private
14
-
15
- def build_type(schema, ast_node)
16
- if ast_node.is_a?(GraphQL::Language::Nodes::TypeName)
3
+ module TypeExpression
4
+ def self.build_type(schema, ast_node)
5
+ case ast_node
6
+ when GraphQL::Language::Nodes::TypeName
17
7
  type_name = ast_node.name
18
8
  schema.types[type_name]
19
- elsif ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
9
+ when GraphQL::Language::Nodes::NonNullType
20
10
  ast_inner_type = ast_node.of_type
21
11
  build_type(schema, ast_inner_type).to_non_null_type
22
- elsif ast_node.is_a?(GraphQL::Language::Nodes::ListType)
12
+ when GraphQL::Language::Nodes::ListType
23
13
  ast_inner_type = ast_node.of_type
24
14
  build_type(schema, ast_inner_type).to_list_type
25
15
  end
@@ -8,7 +8,7 @@ module GraphQL
8
8
  # If you want a type, but want to handle the undefined case, use {#fetch}.
9
9
  class TypeMap
10
10
  extend Forwardable
11
- def_delegators :@storage, :key?, :keys, :values
11
+ def_delegators :@storage, :key?, :keys, :values, :to_h, :fetch
12
12
 
13
13
  def initialize
14
14
  @storage = {}
@@ -25,10 +25,6 @@ module GraphQL
25
25
  @storage[key] = value
26
26
  end
27
27
  end
28
-
29
- def fetch(key, fallback_value)
30
- @storage.key?(key) ? @storage[key] : fallback_value
31
- end
32
28
  end
33
29
  end
34
30
  end
@@ -0,0 +1,164 @@
1
+ module GraphQL
2
+ class Schema
3
+ # This module provides a function for validating GraphQL types.
4
+ #
5
+ # Its {RULES} contain objects that respond to `#call(type)`. Rules are
6
+ # looked up for given types (by class ancestry), then applied to
7
+ # the object until an error is returned.
8
+ class Validation
9
+ # Lookup the rules for `object` based on its class,
10
+ # Then returns an error message or `nil`
11
+ # @param object [Object] something to be validated
12
+ # @return [String, Nil] error message, if there was one
13
+ def self.validate(object)
14
+ rules = RULES.reduce([]) do |memo, (parent_class, validations)|
15
+ memo + (object.is_a?(parent_class) ? validations : [])
16
+ end
17
+ # Stops after the first error
18
+ rules.reduce(nil) { |memo, rule| memo || rule.call(object) }
19
+ end
20
+
21
+ module Rules
22
+ # @param property_name [Symbol] The method to validate
23
+ # @param allowed_classes [Class] Classes which the return value may be an instance of
24
+ # @return [Proc] A proc which will validate the input by calling `property_name` and asserting it is an instance of one of `allowed_classes`
25
+ def self.assert_property(property_name, *allowed_classes)
26
+ allowed_classes_message = allowed_classes.map(&:name).join(" or ")
27
+ -> (obj) {
28
+ property_value = obj.public_send(property_name)
29
+ is_valid_value = allowed_classes.any? { |allowed_class| property_value.is_a?(allowed_class) }
30
+ is_valid_value ? nil : "#{property_name} must return #{allowed_classes_message}, not #{property_value.class.name} (#{property_value.inspect})"
31
+ }
32
+ end
33
+
34
+ # @param property_name [Symbol] The method whose return value will be validated
35
+ # @param from_class [Class] The class for keys in the return value
36
+ # @param to_class [Class] The class for values in the return value
37
+ # @return [Proc] A proc to validate that validates the input by calling `property_name` and asserting that the return value is a Hash of `{from_class => to_class}` pairs
38
+ def self.assert_property_mapping(property_name, from_class, to_class)
39
+ -> (obj) {
40
+ property_value = obj.public_send(property_name)
41
+ error_message = nil
42
+ if !property_value.is_a?(Hash)
43
+ "#{property_name} must be a hash of {#{from_class.name} => #{to_class.name}}, not a #{property_value.class.name} (#{property_value.inspect})"
44
+ else
45
+ invalid_key, invalid_value = property_value.find { |key, value| !key.is_a?(from_class) || !value.is_a?(to_class) }
46
+ if invalid_key
47
+ "#{property_name} must map #{from_class} => #{to_class}, not #{invalid_key.class.name} => #{invalid_value.class.name} (#{invalid_key.inspect} => #{invalid_value.inspect})"
48
+ else
49
+ nil # OK
50
+ end
51
+ end
52
+ }
53
+ end
54
+
55
+ # @param property_name [Symbol] The method whose return value will be validated
56
+ # @param list_member_class [Class] The class which each member of the returned array should be an instance of
57
+ # @return [Proc] A proc to validate the input by calling `property_name` and asserting that the return is an Array of `list_member_class` instances
58
+ def self.assert_property_list_of(property_name, list_member_class)
59
+ -> (obj) {
60
+ property_value = obj.public_send(property_name)
61
+ if !property_value.is_a?(Array)
62
+ "#{property_name} must be an Array of #{list_member_class.name}, not a #{property_value.class.name} (#{property_value.inspect})"
63
+ else
64
+ invalid_member = property_value.find { |value| !value.is_a?(list_member_class) }
65
+ if invalid_member
66
+ "#{property_name} must contain #{list_member_class.name}, not #{invalid_member.class.name} (#{invalid_member.inspect})"
67
+ else
68
+ nil # OK
69
+ end
70
+ end
71
+ }
72
+ end
73
+
74
+ def self.assert_named_items_are_valid(item_name, get_items_proc)
75
+ -> (type) {
76
+ items = get_items_proc.call(type)
77
+ error_message = nil
78
+ items.each do |item|
79
+ item_message = GraphQL::Schema::Validation.validate(item)
80
+ if item_message
81
+ error_message = "#{item_name} #{item.name.inspect} #{item_message}"
82
+ break
83
+ end
84
+ end
85
+ error_message
86
+ }
87
+ end
88
+
89
+
90
+ FIELDS_ARE_VALID = Rules.assert_named_items_are_valid("field", -> (type) { type.all_fields })
91
+
92
+ HAS_ONE_OR_MORE_POSSIBLE_TYPES = -> (type) {
93
+ type.possible_types.length > 1 ? nil : "must have at least one possible type"
94
+ }
95
+
96
+ NAME_IS_STRING = Rules.assert_property(:name, String)
97
+ DESCRIPTION_IS_STRING_OR_NIL = Rules.assert_property(:description, String, NilClass)
98
+ ARGUMENTS_ARE_STRING_TO_ARGUMENT = Rules.assert_property_mapping(:arguments, String, GraphQL::Argument)
99
+ ARGUMENTS_ARE_VALID = Rules.assert_named_items_are_valid("argument", -> (type) { type.arguments.values })
100
+
101
+ DEFAULT_VALUE_IS_VALID_FOR_TYPE = -> (type) {
102
+ if !type.default_value.nil?
103
+ coerced_value = type.type.coerce_input(type.default_value)
104
+ if coerced_value.nil?
105
+ "default value #{type.default_value.inspect} is not valid for type #{type.type}"
106
+ end
107
+ end
108
+ }
109
+
110
+ TYPE_IS_VALID_INPUT_TYPE = -> (type) {
111
+ outer_type = type.type
112
+ inner_type = outer_type.is_a?(GraphQL::BaseType) ? outer_type.unwrap : nil
113
+
114
+ case inner_type
115
+ when GraphQL::ScalarType, GraphQL::InputObjectType, GraphQL::EnumType
116
+ # OK
117
+ else
118
+ "type must be a valid input type (Scalar or InputObject), not #{outer_type.class} (#{outer_type})"
119
+ end
120
+ }
121
+ end
122
+
123
+ # A mapping of `{Class => [Proc, Proc...]}` pairs.
124
+ # To validate an instance, find entries where `object.is_a?(key)` is true.
125
+ # Then apply each rule from the matching values.
126
+ RULES = {
127
+ GraphQL::Field => [
128
+ Rules::NAME_IS_STRING,
129
+ Rules::DESCRIPTION_IS_STRING_OR_NIL,
130
+ Rules.assert_property(:deprecation_reason, String, NilClass),
131
+ Rules.assert_property(:type, GraphQL::BaseType),
132
+ Rules.assert_property(:property, Symbol, NilClass),
133
+ Rules::ARGUMENTS_ARE_STRING_TO_ARGUMENT,
134
+ Rules::ARGUMENTS_ARE_VALID,
135
+ ],
136
+ GraphQL::Argument => [
137
+ Rules::NAME_IS_STRING,
138
+ Rules::DESCRIPTION_IS_STRING_OR_NIL,
139
+ Rules::TYPE_IS_VALID_INPUT_TYPE,
140
+ Rules::DEFAULT_VALUE_IS_VALID_FOR_TYPE,
141
+ ],
142
+ GraphQL::BaseType => [
143
+ Rules::NAME_IS_STRING,
144
+ Rules::DESCRIPTION_IS_STRING_OR_NIL,
145
+ ],
146
+ GraphQL::ObjectType => [
147
+ Rules.assert_property_list_of(:interfaces, GraphQL::InterfaceType),
148
+ Rules::FIELDS_ARE_VALID,
149
+ ],
150
+ GraphQL::InputObjectType => [
151
+ Rules::ARGUMENTS_ARE_STRING_TO_ARGUMENT,
152
+ Rules::ARGUMENTS_ARE_VALID,
153
+ ],
154
+ GraphQL::UnionType => [
155
+ Rules.assert_property_list_of(:possible_types, GraphQL::ObjectType),
156
+ Rules::HAS_ONE_OR_MORE_POSSIBLE_TYPES,
157
+ ],
158
+ GraphQL::InterfaceType => [
159
+ Rules::FIELDS_ARE_VALID,
160
+ ],
161
+ }
162
+ end
163
+ end
164
+ end