graphql 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/base_type.rb +33 -5
  3. data/lib/graphql/boolean_type.rb +1 -0
  4. data/lib/graphql/compatibility/execution_specification.rb +1 -1
  5. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +1 -0
  6. data/lib/graphql/directive.rb +10 -2
  7. data/lib/graphql/directive/deprecated_directive.rb +1 -0
  8. data/lib/graphql/directive/include_directive.rb +1 -0
  9. data/lib/graphql/directive/skip_directive.rb +1 -0
  10. data/lib/graphql/enum_type.rb +1 -0
  11. data/lib/graphql/execution/execute.rb +1 -13
  12. data/lib/graphql/float_type.rb +1 -0
  13. data/lib/graphql/id_type.rb +1 -0
  14. data/lib/graphql/input_object_type.rb +12 -2
  15. data/lib/graphql/int_type.rb +1 -0
  16. data/lib/graphql/interface_type.rb +1 -0
  17. data/lib/graphql/introspection/directive_location_enum.rb +1 -0
  18. data/lib/graphql/introspection/directive_type.rb +1 -0
  19. data/lib/graphql/introspection/enum_value_type.rb +1 -0
  20. data/lib/graphql/introspection/field_type.rb +1 -0
  21. data/lib/graphql/introspection/input_fields_field.rb +1 -1
  22. data/lib/graphql/introspection/input_value_type.rb +1 -0
  23. data/lib/graphql/introspection/schema_type.rb +2 -0
  24. data/lib/graphql/introspection/type_kind_enum.rb +1 -0
  25. data/lib/graphql/introspection/type_type.rb +1 -0
  26. data/lib/graphql/list_type.rb +1 -0
  27. data/lib/graphql/non_null_type.rb +1 -0
  28. data/lib/graphql/object_type.rb +1 -0
  29. data/lib/graphql/query.rb +50 -13
  30. data/lib/graphql/query/context.rb +5 -4
  31. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -22
  32. data/lib/graphql/relay/array_connection.rb +3 -1
  33. data/lib/graphql/relay/connection_type.rb +15 -1
  34. data/lib/graphql/relay/node.rb +1 -0
  35. data/lib/graphql/relay/page_info.rb +1 -0
  36. data/lib/graphql/relay/relation_connection.rb +2 -0
  37. data/lib/graphql/schema.rb +21 -13
  38. data/lib/graphql/schema/catchall_middleware.rb +2 -2
  39. data/lib/graphql/schema/middleware_chain.rb +71 -13
  40. data/lib/graphql/schema/null_mask.rb +10 -0
  41. data/lib/graphql/schema/printer.rb +85 -59
  42. data/lib/graphql/schema/rescue_middleware.rb +2 -2
  43. data/lib/graphql/schema/timeout_middleware.rb +2 -2
  44. data/lib/graphql/schema/validation.rb +1 -12
  45. data/lib/graphql/schema/warden.rb +48 -24
  46. data/lib/graphql/static_validation/literal_validator.rb +2 -2
  47. data/lib/graphql/string_type.rb +1 -0
  48. data/lib/graphql/union_type.rb +7 -1
  49. data/lib/graphql/version.rb +1 -1
  50. data/readme.md +0 -19
  51. data/spec/graphql/directive/skip_directive_spec.rb +8 -0
  52. data/spec/graphql/directive_spec.rb +9 -3
  53. data/spec/graphql/input_object_type_spec.rb +67 -0
  54. data/spec/graphql/query/variables_spec.rb +1 -1
  55. data/spec/graphql/relay/array_connection_spec.rb +9 -0
  56. data/spec/graphql/relay/connection_type_spec.rb +20 -0
  57. data/spec/graphql/relay/node_spec.rb +6 -0
  58. data/spec/graphql/relay/page_info_spec.rb +4 -0
  59. data/spec/graphql/relay/relation_connection_spec.rb +8 -0
  60. data/spec/graphql/scalar_type_spec.rb +4 -0
  61. data/spec/graphql/schema/middleware_chain_spec.rb +27 -13
  62. data/spec/graphql/schema/printer_spec.rb +121 -6
  63. data/spec/graphql/schema/rescue_middleware_spec.rb +4 -4
  64. data/spec/graphql/schema/warden_spec.rb +16 -12
  65. data/spec/graphql/schema_spec.rb +9 -0
  66. data/spec/graphql/string_type_spec.rb +10 -4
  67. data/spec/spec_helper.rb +2 -1
  68. data/spec/support/star_wars_schema.rb +2 -2
  69. metadata +19 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0019d58e7660531d537f4318a9f77df48ee6c501
4
- data.tar.gz: b1b229e62437f03ce0f5003a92c2546b733121ad
3
+ metadata.gz: 54eddb1e7e7ebdbe1ada0ab8286bda07a4141ebc
4
+ data.tar.gz: d9cc5078d078b0d9d8eded46a17ae224d33b738c
5
5
  SHA512:
6
- metadata.gz: 7d9f28b11005ac84e02b4528cd6c75ace38368c7bed476bc08a7c8b5b88e6a2643a2e5a3779da11babcf983aacc30bcc4bf6a69e53da83cc8e0b4ecac042a5f9
7
- data.tar.gz: 22249283811a82bda630d4d5f24149cfbdea7ad2a7aa9a4583810a318f054093c077b87d6f84550c129e3d431f3a504fa0b97ad109c65b1b89dd0f6e7716e256
6
+ metadata.gz: f918ba8ec64d8e3157d454ef76002b4087644fcb1f92a50821f5fd189f40806b4daec24d27049c022a59c27dc9a698742ad099546ce53b20645a7e0f6ebd8c28
7
+ data.tar.gz: 21140eaea9b18fd8405cb195f67d8f380d08dbf7d537e191c4672bd65240b1f589074927e0a62951f6506eaf0762efcd351e35b0e7951c4012b3552d70b747e0
@@ -4,12 +4,22 @@ module GraphQL
4
4
  class BaseType
5
5
  include GraphQL::Define::NonNullWithBang
6
6
  include GraphQL::Define::InstanceDefinable
7
- accepts_definitions :name, :description, {
8
- connection: GraphQL::Define::AssignConnection,
9
- global_id_field: GraphQL::Define::AssignGlobalIdField,
10
- }
7
+ accepts_definitions :name, :description,
8
+ :introspection,
9
+ :default_scalar,
10
+ :default_relay,
11
+ {
12
+ connection: GraphQL::Define::AssignConnection,
13
+ global_id_field: GraphQL::Define::AssignGlobalIdField,
14
+ }
11
15
 
12
- ensure_defined(:name, :description)
16
+ ensure_defined(:name, :description, :introspection?, :default_scalar?)
17
+
18
+ def initialize
19
+ @introspection = false
20
+ @default_scalar = false
21
+ @default_relay = false
22
+ end
13
23
 
14
24
  def initialize_copy(other)
15
25
  super
@@ -24,6 +34,24 @@ module GraphQL
24
34
  # @return [String, nil] a description for this type
25
35
  attr_accessor :description
26
36
 
37
+ # @return [Boolean] Is this type a predefined introspection type?
38
+ def introspection?
39
+ @introspection
40
+ end
41
+
42
+ # @return [Boolean] Is this type a built-in scalar type? (eg, `String`, `Int`)
43
+ def default_scalar?
44
+ @default_scalar
45
+ end
46
+
47
+ # @return [Boolean] Is this type a built-in Relay type? (`Node`, `PageInfo`)
48
+ def default_relay?
49
+ @default_relay
50
+ end
51
+
52
+ # @api private
53
+ attr_writer :introspection, :default_scalar, :default_relay
54
+
27
55
  # @param other [GraphQL::BaseType] compare to this object
28
56
  # @return [Boolean] are these types equivalent? (incl. non-null, list)
29
57
  def ==(other)
@@ -5,4 +5,5 @@ GraphQL::BOOLEAN_TYPE = GraphQL::ScalarType.define do
5
5
 
6
6
  coerce_input ->(value) { (value == true || value == false) ? value : nil }
7
7
  coerce_result ->(value) { !!value }
8
+ default_scalar true
8
9
  end
@@ -198,7 +198,7 @@ module GraphQL
198
198
  end
199
199
 
200
200
  def test_it_applies_masking
201
- no_org = ->(member) { member.name == "Organization" }
201
+ no_org = ->(member, ctx) { member.name == "Organization" }
202
202
  query_string = %|
203
203
  {
204
204
  node(id: "2001") {
@@ -49,6 +49,7 @@ module GraphQL
49
49
  end
50
50
 
51
51
  module TestMiddleware
52
+ # TODO: Once deprecated `next_middleware` argument becomes unsupported, add `&` to the argument
52
53
  def self.call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
53
54
  query_context[:middleware_log] && query_context[:middleware_log] << field_definition.name
54
55
  next_middleware.call
@@ -8,10 +8,12 @@ module GraphQL
8
8
  #
9
9
  class Directive
10
10
  include GraphQL::Define::InstanceDefinable
11
- accepts_definitions :locations, :name, :description, :arguments, argument: GraphQL::Define::AssignArgument
11
+ accepts_definitions :locations, :name, :description, :arguments, :default_directive, argument: GraphQL::Define::AssignArgument
12
12
 
13
13
  attr_accessor :locations, :arguments, :name, :description
14
- ensure_defined(:locations, :arguments, :name, :description)
14
+ # @api private
15
+ attr_writer :default_directive
16
+ ensure_defined(:locations, :arguments, :name, :description, :default_directive?)
15
17
 
16
18
  LOCATIONS = [
17
19
  QUERY = :QUERY,
@@ -58,6 +60,7 @@ module GraphQL
58
60
 
59
61
  def initialize
60
62
  @arguments = {}
63
+ @default_directive = false
61
64
  end
62
65
 
63
66
  def to_s
@@ -75,6 +78,11 @@ module GraphQL
75
78
  def on_operation?
76
79
  locations.include?(QUERY) && locations.include?(MUTATION) && locations.include?(SUBSCRIPTION)
77
80
  end
81
+
82
+ # @return [Boolean] Is this directive supplied by default? (eg `@skip`)
83
+ def default_directive?
84
+ @default_directive
85
+ end
78
86
  end
79
87
  end
80
88
 
@@ -9,4 +9,5 @@ GraphQL::Directive::DeprecatedDirective = GraphQL::Directive.define do
9
9
  "in [Markdown](https://daringfireball.net/projects/markdown/)."
10
10
 
11
11
  argument :reason, GraphQL::STRING_TYPE, reason_description, default_value: GraphQL::Directive::DEFAULT_DEPRECATION_REASON
12
+ default_directive true
12
13
  end
@@ -4,4 +4,5 @@ GraphQL::Directive::IncludeDirective = GraphQL::Directive.define do
4
4
  description "Directs the executor to include this field or fragment only when the `if` argument is true."
5
5
  locations([GraphQL::Directive::FIELD, GraphQL::Directive::FRAGMENT_SPREAD, GraphQL::Directive::INLINE_FRAGMENT])
6
6
  argument :if, !GraphQL::BOOLEAN_TYPE, 'Included when true.'
7
+ default_directive true
7
8
  end
@@ -5,4 +5,5 @@ GraphQL::Directive::SkipDirective = GraphQL::Directive.define do
5
5
  locations([GraphQL::Directive::FIELD, GraphQL::Directive::FRAGMENT_SPREAD, GraphQL::Directive::INLINE_FRAGMENT])
6
6
 
7
7
  argument :if, !GraphQL::BOOLEAN_TYPE, 'Skipped when true.'
8
+ default_directive true
8
9
  end
@@ -76,6 +76,7 @@ module GraphQL
76
76
  ensure_defined(:values, :validate_non_null_input, :coerce_non_null_input, :coerce_result)
77
77
 
78
78
  def initialize
79
+ super
79
80
  @values_by_name = {}
80
81
  end
81
82
 
@@ -57,20 +57,8 @@ module GraphQL
57
57
  )
58
58
 
59
59
  arguments = query.arguments_for(selection.irep_node, field)
60
- middlewares = query.schema.middleware
61
- resolve_arguments = [parent_type, object, field, arguments, field_ctx]
62
-
63
60
  raw_value = begin
64
- # only run a middleware chain if there are any middleware
65
- if middlewares.any?
66
- chain = GraphQL::Schema::MiddlewareChain.new(
67
- steps: middlewares + [FieldResolveStep],
68
- arguments: resolve_arguments
69
- )
70
- chain.call
71
- else
72
- FieldResolveStep.call(*resolve_arguments)
73
- end
61
+ query_ctx.schema.middleware.invoke([parent_type, object, field, arguments, field_ctx])
74
62
  rescue GraphQL::ExecutionError => err
75
63
  err
76
64
  end
@@ -5,4 +5,5 @@ GraphQL::FLOAT_TYPE = GraphQL::ScalarType.define do
5
5
 
6
6
  coerce_input ->(value) { value.is_a?(Numeric) ? value.to_f : nil }
7
7
  coerce_result ->(value) { value.to_f }
8
+ default_scalar true
8
9
  end
@@ -12,4 +12,5 @@ GraphQL::ID_TYPE = GraphQL::ScalarType.define do
12
12
  nil
13
13
  end
14
14
  }
15
+ default_scalar true
15
16
  end
@@ -42,6 +42,7 @@ module GraphQL
42
42
 
43
43
 
44
44
  def initialize
45
+ super
45
46
  @arguments = {}
46
47
  end
47
48
 
@@ -57,7 +58,16 @@ module GraphQL
57
58
  def validate_non_null_input(input, warden)
58
59
  result = GraphQL::Query::InputValidationResult.new
59
60
 
60
- visible_arguments_map = warden.input_fields(self).reduce({}) { |m, f| m[f.name] = f; m}
61
+ if (input.to_h rescue nil).nil?
62
+ result.add_problem(
63
+ "Expected #{JSON.generate(input, quirks_mode: true)} to be a key, value object " \
64
+ " responding to `to_h`."
65
+ )
66
+ return result
67
+ end
68
+
69
+
70
+ visible_arguments_map = warden.arguments(self).reduce({}) { |m, f| m[f.name] = f; m}
61
71
 
62
72
  # Items in the input that are unexpected
63
73
  input.each do |name, value|
@@ -67,7 +77,7 @@ module GraphQL
67
77
  end
68
78
 
69
79
  # Items in the input that are expected, but have invalid values
70
- invalid_fields = visible_arguments_map.map do |name, field|
80
+ visible_arguments_map.map do |name, field|
71
81
  field_result = field.type.validate_input(input[name], warden)
72
82
  if !field_result.valid?
73
83
  result.merge_result!(name, field_result)
@@ -5,4 +5,5 @@ GraphQL::INT_TYPE = GraphQL::ScalarType.define do
5
5
 
6
6
  coerce_input ->(value) { value.is_a?(Numeric) ? value.to_i : nil }
7
7
  coerce_result ->(value) { value.to_i }
8
+ default_scalar true
8
9
  end
@@ -29,6 +29,7 @@ module GraphQL
29
29
  ensure_defined :fields
30
30
 
31
31
  def initialize
32
+ super
32
33
  @fields = {}
33
34
  end
34
35
 
@@ -7,4 +7,5 @@ GraphQL::Introspection::DirectiveLocationEnum = GraphQL::EnumType.define do
7
7
  GraphQL::Directive::LOCATIONS.each do |location|
8
8
  value(location.to_s, GraphQL::Directive::LOCATION_DESCRIPTIONS[location], value: location)
9
9
  end
10
+ introspection true
10
11
  end
@@ -14,4 +14,5 @@ GraphQL::Introspection::DirectiveType = GraphQL::ObjectType.define do
14
14
  field :onOperation, !types.Boolean, deprecation_reason: "Use `locations`.", property: :on_operation?
15
15
  field :onFragment, !types.Boolean, deprecation_reason: "Use `locations`.", property: :on_fragment?
16
16
  field :onField, !types.Boolean, deprecation_reason: "Use `locations`.", property: :on_field?
17
+ introspection true
17
18
  end
@@ -10,4 +10,5 @@ GraphQL::Introspection::EnumValueType = GraphQL::ObjectType.define do
10
10
  resolve ->(obj, a, c) { !!obj.deprecation_reason }
11
11
  end
12
12
  field :deprecationReason, types.String, property: :deprecation_reason
13
+ introspection true
13
14
  end
@@ -11,4 +11,5 @@ GraphQL::Introspection::FieldType = GraphQL::ObjectType.define do
11
11
  resolve ->(obj, a, c) { !!obj.deprecation_reason }
12
12
  end
13
13
  field :deprecationReason, types.String, property: :deprecation_reason
14
+ introspection true
14
15
  end
@@ -4,7 +4,7 @@ GraphQL::Introspection::InputFieldsField = GraphQL::Field.define do
4
4
  type types[!GraphQL::Introspection::InputValueType]
5
5
  resolve ->(target, a, ctx) {
6
6
  if target.kind.input_object?
7
- ctx.warden.input_fields(target)
7
+ ctx.warden.arguments(target)
8
8
  else
9
9
  nil
10
10
  end
@@ -17,4 +17,5 @@ GraphQL::Introspection::InputValueType = GraphQL::ObjectType.define do
17
17
  end
18
18
  }
19
19
  end
20
+ introspection true
20
21
  end
@@ -24,4 +24,6 @@ GraphQL::Introspection::SchemaType = GraphQL::ObjectType.define do
24
24
  field :directives, !types[!GraphQL::Introspection::DirectiveType], "A list of all directives supported by this server." do
25
25
  resolve ->(obj, arg, ctx) { obj.directives.values }
26
26
  end
27
+
28
+ introspection true
27
29
  end
@@ -5,4 +5,5 @@ GraphQL::Introspection::TypeKindEnum = GraphQL::EnumType.define do
5
5
  GraphQL::TypeKinds::TYPE_KINDS.each do |type_kind|
6
6
  value(type_kind.name, type_kind.description)
7
7
  end
8
+ introspection true
8
9
  end
@@ -22,4 +22,5 @@ GraphQL::Introspection::TypeType = GraphQL::ObjectType.define do
22
22
  field :enumValues, GraphQL::Introspection::EnumValuesField
23
23
  field :inputFields, GraphQL::Introspection::InputFieldsField
24
24
  field :ofType, GraphQL::Introspection::OfTypeField
25
+ introspection true
25
26
  end
@@ -28,6 +28,7 @@ module GraphQL
28
28
  include GraphQL::BaseType::ModifiesAnotherType
29
29
  attr_reader :of_type
30
30
  def initialize(of_type:)
31
+ super()
31
32
  @of_type = of_type
32
33
  end
33
34
 
@@ -33,6 +33,7 @@ module GraphQL
33
33
 
34
34
  attr_reader :of_type
35
35
  def initialize(of_type:)
36
+ super()
36
37
  @of_type = of_type
37
38
  end
38
39
 
@@ -35,6 +35,7 @@ module GraphQL
35
35
 
36
36
 
37
37
  def initialize
38
+ super
38
39
  @fields = {}
39
40
  @dirty_interfaces = []
40
41
  end
@@ -18,14 +18,6 @@ module GraphQL
18
18
  end
19
19
  end
20
20
 
21
- module NullExcept
22
- module_function
23
-
24
- def call(member)
25
- false
26
- end
27
- end
28
-
29
21
  attr_reader :schema, :document, :context, :fragments, :operations, :root_value, :max_depth, :query_string, :warden, :provided_variables
30
22
 
31
23
  # Prepare query `query_string` on `schema`
@@ -38,11 +30,19 @@ module GraphQL
38
30
  # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
39
31
  # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
40
32
  # @param except [<#call(schema_member)>] If provided, objects will be hidden from the schema when `.call(schema_member)` returns truthy
41
- def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil, except: NullExcept)
33
+ def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil, except: nil, only: nil)
42
34
  fail ArgumentError, "a query string or document is required" unless query_string || document
43
35
 
44
36
  @schema = schema
45
- @warden = GraphQL::Schema::Warden.new(schema, except)
37
+ mask = if except
38
+ wrap_if_legacy_mask(except)
39
+ elsif only
40
+ InvertedMask.new(wrap_if_legacy_mask(only))
41
+ else
42
+ schema.default_mask
43
+ end
44
+ @context = Context.new(query: self, values: context)
45
+ @warden = GraphQL::Schema::Warden.new(mask, schema: @schema, context: @context)
46
46
  @max_depth = max_depth || schema.max_depth
47
47
  @max_complexity = max_complexity || schema.max_complexity
48
48
  @query_analyzers = schema.query_analyzers.dup
@@ -52,7 +52,6 @@ module GraphQL
52
52
  if @max_complexity
53
53
  @query_analyzers << GraphQL::Analysis::MaxQueryComplexity.new(@max_complexity)
54
54
  end
55
- @context = Context.new(query: self, values: context)
56
55
  @root_value = root_value
57
56
  @operation_name = operation_name
58
57
  @fragments = {}
@@ -74,6 +73,8 @@ module GraphQL
74
73
  end
75
74
  end
76
75
 
76
+ @resolved_types_cache = Hash.new { |h, k| h[k] = @schema.resolve_type(k, @context) }
77
+
77
78
  @arguments_cache = Hash.new { |h, k| h[k] = {} }
78
79
  @validation_errors = []
79
80
  @analysis_errors = []
@@ -214,8 +215,11 @@ module GraphQL
214
215
  @warden.possible_types(type)
215
216
  end
216
217
 
217
- def resolve_type(type)
218
- @schema.resolve_type(type, @context)
218
+ # @param value [Object] Any runtime value
219
+ # @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type}
220
+ # @see {#possible_types} to apply filtering from `only` / `except`
221
+ def resolve_type(value)
222
+ @resolved_types_cache[value]
219
223
  end
220
224
 
221
225
  def mutation?
@@ -257,5 +261,38 @@ module GraphQL
257
261
  operations[operation_name]
258
262
  end
259
263
  end
264
+
265
+ def wrap_if_legacy_mask(mask)
266
+ if (mask.is_a?(Proc) && mask.arity == 1) || mask.method(:call).arity == 1
267
+ warn("Schema.execute(..., except:) filters now accept two arguments, `(member, ctx)`. One-argument filters are deprecated.")
268
+ LegacyMaskWrap.new(mask)
269
+ else
270
+ mask
271
+ end
272
+ end
273
+
274
+ # @api private
275
+ class InvertedMask
276
+ def initialize(inner_mask)
277
+ @inner_mask = inner_mask
278
+ end
279
+
280
+ # Returns true when the inner mask returned false
281
+ # Returns false when the inner mask returned true
282
+ def call(member, ctx)
283
+ !@inner_mask.call(member, ctx)
284
+ end
285
+ end
286
+
287
+ # @api private
288
+ class LegacyMaskWrap
289
+ def initialize(inner_mask)
290
+ @inner_mask = inner_mask
291
+ end
292
+
293
+ def call(member, ctx)
294
+ @inner_mask.call(member)
295
+ end
296
+ end
260
297
  end
261
298
  end
@@ -32,9 +32,6 @@ module GraphQL
32
32
  # @return [GraphQL::Schema]
33
33
  attr_reader :schema
34
34
 
35
- # @return [GraphQL::Schema::Mask::Warden]
36
- attr_reader :warden
37
-
38
35
  # @return [Array<String, Integer>] The current position in the result
39
36
  attr_reader :path
40
37
 
@@ -46,7 +43,6 @@ module GraphQL
46
43
  @schema = query.schema
47
44
  @values = values || {}
48
45
  @errors = []
49
- @warden = query.warden
50
46
  @path = []
51
47
  end
52
48
 
@@ -55,6 +51,11 @@ module GraphQL
55
51
  @values[key]
56
52
  end
57
53
 
54
+ # @return [GraphQL::Schema::Warden]
55
+ def warden
56
+ @warden ||= @query.warden
57
+ end
58
+
58
59
  # Reassign `key` to the hash passed to {Schema#execute} as `context:`
59
60
  def []=(key, value)
60
61
  @values[key] = value