graphql 0.16.1 → 0.17.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/query_complexity.rb +20 -13
  3. data/lib/graphql/analysis/query_depth.rb +2 -2
  4. data/lib/graphql/argument.rb +7 -2
  5. data/lib/graphql/base_type.rb +2 -1
  6. data/lib/graphql/boolean_type.rb +1 -0
  7. data/lib/graphql/define/assign_object_field.rb +19 -6
  8. data/lib/graphql/define/instance_definable.rb +44 -3
  9. data/lib/graphql/directive.rb +2 -6
  10. data/lib/graphql/enum_type.rb +11 -14
  11. data/lib/graphql/field.rb +47 -12
  12. data/lib/graphql/field/resolve.rb +57 -0
  13. data/lib/graphql/float_type.rb +2 -0
  14. data/lib/graphql/id_type.rb +2 -0
  15. data/lib/graphql/input_object_type.rb +5 -1
  16. data/lib/graphql/int_type.rb +2 -0
  17. data/lib/graphql/interface_type.rb +1 -1
  18. data/lib/graphql/internal_representation/node.rb +18 -29
  19. data/lib/graphql/internal_representation/rewrite.rb +7 -4
  20. data/lib/graphql/introspection/field_type.rb +1 -1
  21. data/lib/graphql/introspection/input_value_type.rb +1 -1
  22. data/lib/graphql/language/parser.rb +161 -136
  23. data/lib/graphql/language/parser.y +9 -1
  24. data/lib/graphql/object_type.rb +2 -2
  25. data/lib/graphql/query.rb +4 -4
  26. data/lib/graphql/query/directive_resolution.rb +2 -2
  27. data/lib/graphql/query/serial_execution/execution_context.rb +3 -2
  28. data/lib/graphql/query/serial_execution/field_resolution.rb +2 -5
  29. data/lib/graphql/query/serial_execution/selection_resolution.rb +1 -1
  30. data/lib/graphql/query/serial_execution/value_resolution.rb +2 -2
  31. data/lib/graphql/scalar_type.rb +2 -0
  32. data/lib/graphql/schema/timeout_middleware.rb +1 -0
  33. data/lib/graphql/string_type.rb +2 -0
  34. data/lib/graphql/union_type.rb +1 -1
  35. data/lib/graphql/version.rb +1 -1
  36. data/readme.md +1 -3
  37. data/spec/graphql/analysis/query_complexity_spec.rb +41 -5
  38. data/spec/graphql/define/instance_definable_spec.rb +2 -2
  39. data/spec/graphql/field_spec.rb +18 -0
  40. data/spec/graphql/internal_representation/rewrite_spec.rb +7 -6
  41. data/spec/graphql/language/parser_spec.rb +5 -0
  42. data/spec/graphql/query/serial_execution/execution_context_spec.rb +2 -1
  43. data/spec/graphql/schema/timeout_middleware_spec.rb +29 -28
  44. data/spec/support/dairy_app.rb +12 -10
  45. metadata +3 -3
  46. data/lib/graphql/internal_representation/definition.rb +0 -0
@@ -169,7 +169,15 @@ rule
169
169
  object_value_field:
170
170
  name COLON input_value { return make_node(:Argument, name: val[0], value: val[2], position_source: val[0])}
171
171
 
172
- enum_value: IDENTIFIER { return make_node(:Enum, name: val[0], position_source: val[0])}
172
+ enum_value: enum_name { return make_node(:Enum, name: val[0], position_source: val[0])}
173
+
174
+ enum_name: /* any identifier, but not "true", "false" or "null" */
175
+ IDENTIFIER
176
+ | FRAGMENT
177
+ | ON
178
+ | QUERY
179
+ | MUTATION
180
+ | SUBSCRIPTION
173
181
 
174
182
  directives_list_opt:
175
183
  /* none */ { return [] }
@@ -22,10 +22,9 @@ module GraphQL
22
22
  #
23
23
  class ObjectType < GraphQL::BaseType
24
24
  accepts_definitions :interfaces, field: GraphQL::Define::AssignObjectField
25
- attr_accessor :name, :description
26
25
 
27
26
  # @return [Hash<String => GraphQL::Field>] Map String fieldnames to their {GraphQL::Field} implementations
28
- attr_accessor :fields
27
+ lazy_defined_attr_accessor :fields
29
28
 
30
29
  def initialize
31
30
  @fields = {}
@@ -40,6 +39,7 @@ module GraphQL
40
39
 
41
40
  def interfaces
42
41
  @clean_interfaces ||= begin
42
+ ensure_defined
43
43
  @dirty_interfaces.map { |i_type| GraphQL::BaseType.resolve_related_type(i_type) }
44
44
  rescue
45
45
  @dirty_interfaces
data/lib/graphql/query.rb CHANGED
@@ -51,7 +51,7 @@ module GraphQL
51
51
  end
52
52
  end
53
53
 
54
- @arguments_cache = {}
54
+ @arguments_cache = Hash.new { |h, k| h[k] = {} }
55
55
  end
56
56
 
57
57
  # Get the result for this query, executing it once
@@ -103,11 +103,11 @@ module GraphQL
103
103
 
104
104
  # Node-level cache for calculating arguments. Used during execution and query analysis.
105
105
  # @return [GraphQL::Query::Arguments] Arguments for this node, merging default values, literal values and query variables
106
- def arguments_for(irep_node)
107
- @arguments_cache[irep_node] ||= begin
106
+ def arguments_for(irep_node, definition)
107
+ @arguments_cache[irep_node][definition] ||= begin
108
108
  GraphQL::Query::LiteralInput.from_arguments(
109
109
  irep_node.ast_node.arguments,
110
- irep_node.definition.arguments,
110
+ definition.arguments,
111
111
  self.variables
112
112
  )
113
113
  end
@@ -3,8 +3,8 @@ module GraphQL
3
3
  module DirectiveResolution
4
4
  def self.include_node?(irep_node, query)
5
5
  irep_node.directives.each do |directive_node|
6
- directive_defn = directive_node.definition
7
- args = query.arguments_for(directive_node)
6
+ directive_defn = directive_node.definitions.first
7
+ args = query.arguments_for(directive_node, directive_defn)
8
8
  if !directive_defn.include?(args)
9
9
  return false
10
10
  end
@@ -18,8 +18,9 @@ module GraphQL
18
18
  @query.fragments[name]
19
19
  end
20
20
 
21
- def get_field(type, name)
22
- @schema.get_field(type, name)
21
+ def get_field(type, irep_node)
22
+ # fall back for dynamic fields (eg __typename)
23
+ irep_node.definitions[type] || @schema.get_field(type, irep_node.definition_name) || raise("No field found on #{type.name} for '#{irep_node.definition_name}' (#{irep_node.ast_node.name})")
23
24
  end
24
25
 
25
26
  def add_error(err)
@@ -9,11 +9,8 @@ module GraphQL
9
9
  @parent_type = parent_type
10
10
  @target = target
11
11
  @execution_context = execution_context
12
- @field = execution_context.get_field(parent_type, irep_node.definition.name)
13
- if @field.nil?
14
- raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
15
- end
16
- @arguments = execution_context.query.arguments_for(irep_node)
12
+ @field = execution_context.get_field(parent_type, irep_node)
13
+ @arguments = execution_context.query.arguments_for(irep_node, @field)
17
14
  end
18
15
 
19
16
  def result
@@ -32,7 +32,7 @@ module GraphQL
32
32
  end
33
33
 
34
34
  def applies_to_type?(irep_node, type, target)
35
- irep_node.on_types.any? { |child_type|
35
+ irep_node.definitions.any? { |child_type, field_defn|
36
36
  GraphQL::Query::TypeResolver.new(target, child_type, type, execution_context.query.context).type
37
37
  }
38
38
  end
@@ -57,7 +57,7 @@ module GraphQL
57
57
  resolved_type = field_type.resolve_type(value, execution_context)
58
58
 
59
59
  unless resolved_type.is_a?(GraphQL::ObjectType)
60
- raise GraphQL::ObjectType::UnresolvedTypeError.new(irep_node.definition.name, field_type, parent_type)
60
+ raise GraphQL::ObjectType::UnresolvedTypeError.new(irep_node.definition_name, field_type, parent_type)
61
61
  end
62
62
 
63
63
  strategy_class = get_strategy_for_kind(resolved_type.kind)
@@ -82,7 +82,7 @@ module GraphQL
82
82
  # Get the "wrapped" type and resolve the value according to that type
83
83
  def result
84
84
  if value.nil? || value.is_a?(GraphQL::ExecutionError)
85
- raise GraphQL::InvalidNullError.new(irep_node.definition.name, value)
85
+ raise GraphQL::InvalidNullError.new(irep_node.definition_name, value)
86
86
  else
87
87
  wrapped_type = field_type.of_type
88
88
  strategy_class = get_strategy_for_kind(wrapped_type.kind)
@@ -27,6 +27,7 @@ module GraphQL
27
27
  end
28
28
 
29
29
  def coerce_non_null_input(value)
30
+ ensure_defined
30
31
  @coerce_input_proc.call(value)
31
32
  end
32
33
 
@@ -37,6 +38,7 @@ module GraphQL
37
38
  end
38
39
 
39
40
  def coerce_result(value)
41
+ ensure_defined
40
42
  @coerce_result_proc.call(value)
41
43
  end
42
44
 
@@ -33,6 +33,7 @@ module GraphQL
33
33
 
34
34
  def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
35
35
  timeout_at = query_context[@context_key] ||= Time.now + @max_seconds
36
+
36
37
  if timeout_at < Time.now
37
38
  on_timeout(parent_type, parent_object, field_definition, field_args, query_context)
38
39
  else
@@ -1,5 +1,7 @@
1
1
  GraphQL::STRING_TYPE = GraphQL::ScalarType.define do
2
2
  name "String"
3
+ description "Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text."
4
+
3
5
  coerce_result -> (value) { value.to_s }
4
6
  coerce_input -> (value) { value.is_a?(String) ? value : nil }
5
7
  end
@@ -11,7 +11,6 @@ module GraphQL
11
11
  #
12
12
  class UnionType < GraphQL::BaseType
13
13
  include GraphQL::BaseType::HasPossibleTypes
14
- attr_accessor :name, :description
15
14
  accepts_definitions :possible_types, :resolve_type
16
15
 
17
16
  def kind
@@ -29,6 +28,7 @@ module GraphQL
29
28
 
30
29
  def possible_types
31
30
  @clean_possible_types ||= begin
31
+ ensure_defined
32
32
  @dirty_possible_types.map { |type| GraphQL::BaseType.resolve_related_type(type) }
33
33
  rescue
34
34
  @dirty_possible_types
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.16.1"
2
+ VERSION = "0.17.0"
3
3
  end
data/readme.md CHANGED
@@ -127,9 +127,7 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
127
127
  - Problem: how does a field know which schema to look up the name from?
128
128
  - Problem: how can we load types in Rails without accessing the constant?
129
129
  - Maybe support by third-party library? `type("Post!")` could implement "type_missing", keeps `graphql-ruby` very simple
130
- - If we eval'd `define { ... }` blocks _lazily_, would that work around all circular dependency issues?
131
- - `QueryComplexity` improvements:
132
- - Better detection of union / interface possibilities? Right now they're summed
133
130
  - Type check improvements:
134
131
  - Use catch-all type/field/argument definitions instead of terminating traversal
135
132
  - Reduce ad-hoc traversals?
133
+ - Merge `graphql-relay-ruby` into this Repo so that they can stay in sync?
@@ -191,28 +191,47 @@ describe GraphQL::Analysis::QueryComplexity do
191
191
  describe "custom complexities" do
192
192
  let(:query) { GraphQL::Query.new(complexity_schema, query_string) }
193
193
  let(:complexity_schema) {
194
- complexity_type = GraphQL::ObjectType.define do
195
- name "Complexity"
194
+ complexity_interface = GraphQL::InterfaceType.define do
195
+ name "ComplexityInterface"
196
+ field :value, types.Int
197
+ end
198
+
199
+ single_complexity_type = GraphQL::ObjectType.define do
200
+ name "SingleComplexity"
196
201
  field :value, types.Int, complexity: 0.1 do
197
202
  resolve -> (obj, args, ctx) { obj }
198
203
  end
199
- field :complexity, -> { complexity_type } do
204
+ field :complexity, single_complexity_type do
200
205
  argument :value, types.Int
201
206
  complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
202
207
  resolve -> (obj, args, ctx) { args[:value] }
203
208
  end
209
+ interfaces [complexity_interface]
210
+ end
211
+
212
+ double_complexity_type = GraphQL::ObjectType.define do
213
+ name "DoubleComplexity"
214
+ field :value, types.Int, complexity: 4 do
215
+ resolve -> (obj, args, ctx) { obj }
216
+ end
217
+ interfaces [complexity_interface]
204
218
  end
205
219
 
206
220
  query_type = GraphQL::ObjectType.define do
207
221
  name "Query"
208
- field :complexity, -> { complexity_type } do
222
+ field :complexity, single_complexity_type do
209
223
  argument :value, types.Int
210
224
  complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
211
225
  resolve -> (obj, args, ctx) { args[:value] }
212
226
  end
227
+
228
+ field :innerComplexity, complexity_interface do
229
+ argument :value, types.Int
230
+ resolve -> (obj, args, ctx) { args[:value] }
231
+ end
213
232
  end
214
233
 
215
- GraphQL::Schema.new(query: query_type)
234
+ GraphQL::Schema.new(query: query_type, types: [double_complexity_type])
216
235
  }
217
236
  let(:query_string) {%|
218
237
  {
@@ -231,5 +250,22 @@ describe GraphQL::Analysis::QueryComplexity do
231
250
  # 10 from `complexity`, `0.3` from `value`
232
251
  assert_equal complexities, [query, 10.3]
233
252
  end
253
+
254
+ describe "same field on multiple types" do
255
+ let(:query_string) {%|
256
+ {
257
+ innerComplexity(value: 2) {
258
+ ... on SingleComplexity { value }
259
+ ... on DoubleComplexity { value }
260
+ }
261
+ }
262
+ |}
263
+
264
+ it "picks them max for those fields" do
265
+ reduce_result
266
+ # 1 for innerComplexity + 4 for DoubleComplexity.value
267
+ assert_equal complexities, [query, 5]
268
+ end
269
+ end
234
270
  end
235
271
  end
@@ -10,12 +10,12 @@ module Garden
10
10
  end
11
11
 
12
12
  class Vegetable
13
- attr_accessor :name, :start_planting_on, :end_planting_on
14
13
  include GraphQL::Define::InstanceDefinable
14
+ lazy_defined_attr_accessor :name, :start_planting_on, :end_planting_on
15
15
  accepts_definitions :name, plant_between: DefinePlantBetween
16
16
 
17
17
  # definition added later:
18
- attr_accessor :height
18
+ lazy_defined_attr_accessor :height
19
19
  end
20
20
  end
21
21
 
@@ -89,4 +89,22 @@ describe GraphQL::Field do
89
89
  assert_equal "QueryType is invalid: field :symbol_name name must return String, not Symbol (:symbol_name)", err.message
90
90
  end
91
91
  end
92
+
93
+ describe "#hash_key" do
94
+ let(:source_field) { MilkType.get_field("source") }
95
+ after { source_field.hash_key = :source }
96
+
97
+ it "looks up a value with obj[hash_key]" do
98
+ resolved_source = source_field.resolve({source: "Abc", "source" => "Xyz"}, nil, nil)
99
+ assert_equal :source, source_field.hash_key
100
+ assert_equal "Abc", resolved_source
101
+ end
102
+
103
+ it "can be reassigned" do
104
+ source_field.hash_key = "source"
105
+ resolved_source = source_field.resolve({source: "Abc", "source" => "Xyz"}, nil, nil)
106
+ assert_equal "source", source_field.hash_key
107
+ assert_equal "Xyz", resolved_source
108
+ end
109
+ end
92
110
  end
@@ -26,12 +26,12 @@ describe GraphQL::InternalRepresentation::Rewrite do
26
26
  assert_equal QueryType, op_node.return_type
27
27
  first_field = op_node.children.values.first
28
28
  assert_equal 3, first_field.children.length
29
- assert_equal [QueryType], first_field.on_types.to_a
29
+ assert_equal [QueryType], first_field.definitions.keys
30
30
  assert_equal CheeseType, first_field.return_type
31
31
 
32
32
  second_field = op_node.children.values.last
33
33
  assert_equal 1, second_field.children.length
34
- assert_equal [QueryType], second_field.on_types.to_a
34
+ assert_equal [QueryType], second_field.definitions.keys
35
35
  assert_equal CheeseType, second_field.return_type
36
36
  end
37
37
  end
@@ -48,7 +48,8 @@ describe GraphQL::InternalRepresentation::Rewrite do
48
48
  it "gets dynamic field definitions" do
49
49
  cheese_field = rewrite_result[nil].children["cheese"]
50
50
  typename_field = cheese_field.children["typename"]
51
- assert_equal "__typename", typename_field.definition.name
51
+ assert_equal "__typename", typename_field.definitions.values.first.name
52
+ assert_equal "__typename", typename_field.definition_name
52
53
  end
53
54
  end
54
55
 
@@ -112,9 +113,9 @@ describe GraphQL::InternalRepresentation::Rewrite do
112
113
  similar_sheep_field = similar_cow_field.children["similarCheese"]
113
114
  assert_equal ["flavor", "source"], similar_sheep_field.children.keys
114
115
 
115
- assert_equal Set.new([EdibleInterface]), cheese_field.children["origin"].on_types
116
- assert_equal Set.new([CheeseType, EdibleInterface]), cheese_field.children["fatContent"].on_types
117
- assert_equal Set.new([CheeseType]), cheese_field.children["flavor"].on_types
116
+ assert_equal [EdibleInterface], cheese_field.children["origin"].definitions.keys
117
+ assert_equal [CheeseType, EdibleInterface], cheese_field.children["fatContent"].definitions.keys
118
+ assert_equal [CheeseType], cheese_field.children["flavor"].definitions.keys
118
119
  end
119
120
  end
120
121
  end
@@ -190,6 +190,7 @@ describe GraphQL::Language::Parser do
190
190
  array: [7, 8, 9]
191
191
  object: {a: [1,2,3], b: {c: "4"}}
192
192
  unicode_bom: "\xef\xbb\xbfquery"
193
+ keywordEnum: on
193
194
  )
194
195
  }
195
196
  |}
@@ -234,6 +235,10 @@ describe GraphQL::Language::Parser do
234
235
  obj = inputs[7].value
235
236
  assert_equal %|\xef\xbb\xbfquery|, inputs[7].value
236
237
  end
238
+
239
+ it "parses enum 'on''" do
240
+ assert_equal "on", inputs[8].value.name
241
+ end
237
242
  end
238
243
  end
239
244
 
@@ -39,7 +39,8 @@ describe GraphQL::Query::SerialExecution::ExecutionContext do
39
39
 
40
40
  describe "get_field" do
41
41
  it "returns the respective field from the schema" do
42
- field = execution_context.get_field(DairyType, "cheese")
42
+ irep_node = OpenStruct.new(definition_name: "cheese", definitions: {DairyType => DairyType.fields["cheese"]})
43
+ field = execution_context.get_field(DairyType, irep_node)
43
44
  assert_equal("cheese", field.name)
44
45
  end
45
46
  end
@@ -1,8 +1,8 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe GraphQL::Schema::TimeoutMiddleware do
4
- let(:max_seconds) { 2 }
5
- let(:timeout_middleware) { GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2) }
4
+ let(:max_seconds) { 1 }
5
+ let(:timeout_middleware) { GraphQL::Schema::TimeoutMiddleware.new(max_seconds: max_seconds) }
6
6
  let(:timeout_schema) {
7
7
 
8
8
  sleep_for_seconds_resolve = -> (obj, args, ctx) {
@@ -45,18 +45,18 @@ describe GraphQL::Schema::TimeoutMiddleware do
45
45
  describe "timeout part-way through" do
46
46
  let(:query_string) {%|
47
47
  {
48
- a: sleepFor(seconds: 0.7)
49
- b: sleepFor(seconds: 0.7)
50
- c: sleepFor(seconds: 0.7)
51
- d: sleepFor(seconds: 0.7)
52
- e: sleepFor(seconds: 0.7)
48
+ a: sleepFor(seconds: 0.4)
49
+ b: sleepFor(seconds: 0.4)
50
+ c: sleepFor(seconds: 0.4)
51
+ d: sleepFor(seconds: 0.4)
52
+ e: sleepFor(seconds: 0.4)
53
53
  }
54
54
  |}
55
55
  it "returns a partial response and error messages" do
56
56
  expected_data = {
57
- "a"=>0.7,
58
- "b"=>0.7,
59
- "c"=>0.7,
57
+ "a"=>0.4,
58
+ "b"=>0.4,
59
+ "c"=>0.4,
60
60
  "d"=>nil,
61
61
  "e"=>nil,
62
62
  }
@@ -79,11 +79,11 @@ describe GraphQL::Schema::TimeoutMiddleware do
79
79
  describe "timeout in nested fields" do
80
80
  let(:query_string) {%|
81
81
  {
82
- a: nestedSleep(seconds: 1) {
82
+ a: nestedSleep(seconds: 0.3) {
83
83
  seconds
84
- b: nestedSleep(seconds: 0.4) {
84
+ b: nestedSleep(seconds: 0.3) {
85
85
  seconds
86
- c: nestedSleep(seconds: 0.4) {
86
+ c: nestedSleep(seconds: 0.3) {
87
87
  seconds
88
88
  d: nestedSleep(seconds: 0.4) {
89
89
  seconds
@@ -96,14 +96,15 @@ describe GraphQL::Schema::TimeoutMiddleware do
96
96
  }
97
97
  }
98
98
  |}
99
+
99
100
  it "returns a partial response and error messages" do
100
101
  expected_data = {
101
102
  "a" => {
102
- "seconds" => 1.0,
103
+ "seconds" => 0.3,
103
104
  "b" => {
104
- "seconds" => 0.4,
105
+ "seconds" => 0.3,
105
106
  "c" => {
106
- "seconds"=>0.4,
107
+ "seconds"=>0.3,
107
108
  "d" => {
108
109
  "seconds"=>nil,
109
110
  "e"=>nil
@@ -131,17 +132,17 @@ describe GraphQL::Schema::TimeoutMiddleware do
131
132
  describe "long-running fields" do
132
133
  let(:query_string) {%|
133
134
  {
134
- a: sleepFor(seconds: 0.7)
135
- b: sleepFor(seconds: 0.7)
136
- c: sleepFor(seconds: 1.5)
135
+ a: sleepFor(seconds: 0.2)
136
+ b: sleepFor(seconds: 0.2)
137
+ c: sleepFor(seconds: 0.8)
137
138
  d: sleepFor(seconds: 0.1)
138
139
  }
139
140
  |}
140
141
  it "doesn't terminate long-running field execution" do
141
142
  expected_data = {
142
- "a"=>0.7,
143
- "b"=>0.7,
144
- "c"=>1.5,
143
+ "a"=>0.2,
144
+ "b"=>0.2,
145
+ "c"=>0.8,
145
146
  "d"=>nil,
146
147
  }
147
148
 
@@ -159,17 +160,17 @@ describe GraphQL::Schema::TimeoutMiddleware do
159
160
 
160
161
  describe "with a custom block" do
161
162
  let(:timeout_middleware) {
162
- GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2) do |err, query|
163
+ GraphQL::Schema::TimeoutMiddleware.new(max_seconds: max_seconds) do |err, query|
163
164
  raise("Query timed out after 2s: #{query.class.name}")
164
165
  end
165
166
  }
166
167
  let(:query_string) {%|
167
168
  {
168
- a: sleepFor(seconds: 0.7)
169
- b: sleepFor(seconds: 0.7)
170
- c: sleepFor(seconds: 0.7)
171
- d: sleepFor(seconds: 0.7)
172
- e: sleepFor(seconds: 0.7)
169
+ a: sleepFor(seconds: 0.4)
170
+ b: sleepFor(seconds: 0.4)
171
+ c: sleepFor(seconds: 0.4)
172
+ d: sleepFor(seconds: 0.4)
173
+ e: sleepFor(seconds: 0.4)
173
174
  }
174
175
  |}
175
176