graphql 0.16.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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