cucumber_factory 2.0.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,6 +13,7 @@ require 'cucumber_priority'
13
13
 
14
14
  # Gem
15
15
  require 'cucumber_factory/build_strategy'
16
+ require 'cucumber_factory/update_strategy'
16
17
  require 'cucumber_factory/factory'
17
18
  require 'cucumber_factory/switcher'
18
19
 
@@ -7,79 +7,115 @@ module CucumberFactory
7
7
  class << self
8
8
 
9
9
  def from_prose(model_prose, variant_prose)
10
- # don't use \w which depends on the system locale
11
- underscored_model_name = model_prose.gsub(/[^A-Za-z0-9_\/]+/, "_")
12
- if variant_prose.present?
13
- variants = /\((.*?)\)/.match(variant_prose)[1].split(/\s*,\s*/)
14
- variants = variants.collect { |variant| variant.downcase.gsub(" ", "_") }
10
+ variants = variants_from_prose(variant_prose)
11
+ factory = factory_bot_factory(model_prose, variants)
12
+
13
+ if factory
14
+ factory.compile # Required to load inherited traits!
15
+ strategy = factory_bot_strategy(factory, model_prose, variants)
16
+ transient_attributes = factory_bot_transient_attributes(factory, variants)
15
17
  else
16
- variants = []
18
+ strategy = alternative_strategy(model_prose, variants)
19
+ transient_attributes = []
17
20
  end
18
21
 
19
- if factory_bot_strategy = factory_bot_strategy(underscored_model_name, variants)
20
- factory_bot_strategy
22
+ [strategy, transient_attributes]
23
+ end
24
+
25
+ def parse_model_class(model_prose)
26
+ underscored_model_name(model_prose).camelize.constantize
27
+ end
28
+
29
+ private
30
+
31
+ def variants_from_prose(variant_prose)
32
+ if variant_prose.present?
33
+ variants = /\((.*?)\)/.match(variant_prose)[1].split(/\s*,\s*/)
34
+ variants.collect { |variant| variant.downcase.gsub(" ", "_").to_sym }
21
35
  else
22
- model_class = underscored_model_name.camelize.constantize
23
- machinist_strategy(model_class, variants) ||
24
- active_record_strategy(model_class) ||
25
- ruby_object_strategy(model_class)
36
+ []
26
37
  end
27
38
  end
28
39
 
29
- private
40
+ def factory_bot_factory(model_prose, variants)
41
+ return unless factory_bot_class
30
42
 
31
- def factory_bot_strategy(factory_name, variants)
32
- factory_class = ::FactoryBot if defined?(FactoryBot)
33
- factory_class ||= ::FactoryGirl if defined?(FactoryGirl)
34
- return unless factory_class
43
+ factory_name = factory_name_from_prose(model_prose)
44
+ factory = factory_bot_class.factories[factory_name]
35
45
 
36
- variants = variants.map(&:to_sym)
37
- factory_name = factory_name.to_s.underscore.gsub('/', '_').to_sym
46
+ if factory.nil? && variants.present?
47
+ factory = factory_bot_class.factories[variants[0]]
48
+ end
38
49
 
39
- factory = factory_class.factories[factory_name]
50
+ factory
51
+ end
40
52
 
41
- if factory.nil? && variants.present? && factory = factory_class.factories[variants[0]]
53
+ def factory_bot_strategy(factory, model_prose, variants)
54
+ return unless factory
55
+
56
+ factory_name = factory_name_from_prose(model_prose)
57
+ if factory_bot_class.factories[factory_name].nil? && variants.present?
42
58
  factory_name, *variants = variants
43
59
  end
44
60
 
45
- if factory
46
- new(factory.build_class) do |attributes|
47
- # Cannot have additional scalar args after a varargs
48
- # argument in Ruby 1.8 and 1.9
49
- args = []
50
- args += variants
51
- args << attributes
52
- factory_class.create(factory_name, *args)
53
- end
61
+ new(factory.build_class) do |attributes|
62
+ # Cannot have additional scalar args after a varargs
63
+ # argument in Ruby 1.8 and 1.9
64
+ args = []
65
+ args += variants
66
+ args << attributes
67
+ factory_bot_class.create(factory_name, *args)
68
+ end
69
+ end
70
+
71
+ def factory_bot_transient_attributes(factory, variants)
72
+ return [] unless factory
73
+
74
+ factory_attributes = factory_bot_attributes(factory, variants)
75
+ class_attributes = factory.build_class.attribute_names.map(&:to_sym)
76
+
77
+ factory_attributes - class_attributes
78
+ end
79
+
80
+ def factory_bot_attributes(factory, variants)
81
+ traits = factory_bot_traits(factory, variants)
82
+ factory.with_traits(traits.map(&:name)).definition.attributes.names
83
+ end
84
+
85
+ def factory_bot_traits(factory, variants)
86
+ factory.definition.defined_traits.select do |trait|
87
+ variants.include?(trait.name.to_sym)
54
88
  end
89
+ end
55
90
 
91
+ def alternative_strategy(model_prose, variants)
92
+ model_class = parse_model_class(model_prose)
93
+ machinist_strategy(model_class, variants) ||
94
+ active_record_strategy(model_class) ||
95
+ ruby_object_strategy(model_class)
56
96
  end
57
97
 
58
98
  def machinist_strategy(model_class, variants)
59
- if model_class.respond_to?(:make)
60
-
61
- new(model_class) do |attributes|
62
- if variants.present?
63
- variants.size == 1 or raise 'Machinist only supports a single variant per blueprint'
64
- model_class.make(variants.first.to_sym, attributes)
65
- else
66
- model_class.make(attributes)
67
- end
68
- end
99
+ return unless model_class.respond_to?(:make)
69
100
 
101
+ new(model_class) do |attributes|
102
+ if variants.present?
103
+ variants.size == 1 or raise 'Machinist only supports a single variant per blueprint'
104
+ model_class.make(variants.first, attributes)
105
+ else
106
+ model_class.make(attributes)
107
+ end
70
108
  end
71
109
  end
72
110
 
73
111
  def active_record_strategy(model_class)
74
- if model_class.respond_to?(:create!)
75
-
76
- new(model_class) do |attributes|
77
- model = model_class.new
78
- CucumberFactory::Switcher.assign_attributes(model, attributes)
79
- model.save!
80
- model
81
- end
112
+ return unless model_class.respond_to?(:create!)
82
113
 
114
+ new(model_class) do |attributes|
115
+ model = model_class.new
116
+ CucumberFactory::Switcher.assign_attributes(model, attributes)
117
+ model.save!
118
+ model
83
119
  end
84
120
  end
85
121
 
@@ -89,6 +125,21 @@ module CucumberFactory
89
125
  end
90
126
  end
91
127
 
128
+ def factory_bot_class
129
+ factory_class = ::FactoryBot if defined?(FactoryBot)
130
+ factory_class ||= ::FactoryGirl if defined?(FactoryGirl)
131
+ factory_class
132
+ end
133
+
134
+ def factory_name_from_prose(model_prose)
135
+ underscored_model_name(model_prose).to_s.underscore.gsub('/', '_').to_sym
136
+ end
137
+
138
+ def underscored_model_name(model_prose)
139
+ # don't use \w which depends on the system locale
140
+ model_prose.gsub(/[^A-Za-z0-9_\/]+/, "_")
141
+ end
142
+
92
143
  end
93
144
 
94
145
 
@@ -2,21 +2,25 @@ module CucumberFactory
2
2
  module Factory
3
3
  class Error < StandardError; end
4
4
 
5
- ATTRIBUTES_PATTERN = '( with the .+?)?( (?:which|who|that) is .+?)?'
5
+ ATTRIBUTES_PATTERN = '( with the .+?)?( (?:which|who|that) is .+?)?' # ... with the year 1979 which is science fiction
6
6
  TEXT_ATTRIBUTES_PATTERN = ' (?:with|and) these attributes:'
7
+ UPDATE_ATTR_PATTERN = '(?: (?:has|belongs to)( the .+?))?(?:(?: and| but|,)*( is .+?))?' # ... belongs to the collection "Fantasy" and is trending
8
+ TEXT_UPDATE_ATTR_PATTERN = '(?: and|,)* has these attributes:'
7
9
 
8
- RECORD_PATTERN = 'there is an? (.+?)( \(.+?\))?'
9
- NAMED_RECORD_PATTERN = '(?:"([^\"]*)"|\'([^\']*)\') is an? (.+?)( \(.+?\))?'
10
+ RECORD_PATTERN = 'there is an? (.+?)( \(.+?\))?' # Given there is a movie (comedy)
11
+ NAMED_RECORD_PATTERN = '(?:"([^\"]*)"|\'([^\']*)\') is an? (.+?)( \(.+?\))?' # Given "LotR" is a movie
12
+ RECORD_UPDATE_PATTERN = 'the (.+?) (above|".+?"|\'.+?\')' # Given the movie "LotR" ...
10
13
 
11
14
  NAMED_RECORDS_VARIABLE = :'@named_cucumber_factory_records'
12
15
 
13
16
  VALUE_INTEGER = /\d+/
14
17
  VALUE_DECIMAL = /[\d\.]+/
15
18
  VALUE_STRING = /"[^"]*"|'[^']*'/
19
+ VALUE_FILE = /file:#{VALUE_STRING}/
16
20
  VALUE_ARRAY = /\[[^\]]*\]/
17
21
  VALUE_LAST_RECORD = /\babove\b/
18
22
 
19
- VALUE_SCALAR = /#{VALUE_STRING}|#{VALUE_DECIMAL}|#{VALUE_INTEGER}/
23
+ VALUE_SCALAR = /#{VALUE_STRING}|#{VALUE_DECIMAL}|#{VALUE_INTEGER}|#{VALUE_FILE}/
20
24
 
21
25
  CLEAR_NAMED_RECORDS_STEP_DESCRIPTOR = {
22
26
  :kind => :Before,
@@ -39,6 +43,12 @@ module CucumberFactory
39
43
  :block => lambda { |a1, a2, a3, a4| CucumberFactory::Factory.send(:parse_creation, self, a1, a2, a3, a4) }
40
44
  }
41
45
 
46
+ UPDATE_STEP_DESCRIPTOR = {
47
+ :kind => :And,
48
+ :pattern => /^#{RECORD_UPDATE_PATTERN}#{UPDATE_ATTR_PATTERN}$/,
49
+ :block => lambda { |a1, a2, a3, a4| CucumberFactory::Factory.send(:parse_update, self, a1, a2, a3, a4) }
50
+ }
51
+
42
52
  NAMED_CREATION_STEP_DESCRIPTOR_WITH_TEXT_ATTRIBUTES = {
43
53
  :kind => :Given,
44
54
  :pattern => /^#{NAMED_RECORD_PATTERN}#{ATTRIBUTES_PATTERN}#{TEXT_ATTRIBUTES_PATTERN}?$/,
@@ -53,6 +63,13 @@ module CucumberFactory
53
63
  :priority => true
54
64
  }
55
65
 
66
+ UPDATE_STEP_DESCRIPTOR_WITH_TEXT_ATTRIBUTES = {
67
+ :kind => :And,
68
+ :pattern => /^#{RECORD_UPDATE_PATTERN}#{UPDATE_ATTR_PATTERN}#{TEXT_UPDATE_ATTR_PATTERN}$/,
69
+ :block => lambda { |a1, a2, a3, a4, a5| CucumberFactory::Factory.send(:parse_update, self, a1, a2, a3, a4, a5) },
70
+ :priority => true
71
+ }
72
+
56
73
  class << self
57
74
 
58
75
  def add_steps(main)
@@ -61,6 +78,8 @@ module CucumberFactory
61
78
  add_step(main, CREATION_STEP_DESCRIPTOR_WITH_TEXT_ATTRIBUTES)
62
79
  add_step(main, NAMED_CREATION_STEP_DESCRIPTOR_WITH_TEXT_ATTRIBUTES)
63
80
  add_step(main, CLEAR_NAMED_RECORDS_STEP_DESCRIPTOR)
81
+ add_step(main, UPDATE_STEP_DESCRIPTOR)
82
+ add_step(main, UPDATE_STEP_DESCRIPTOR_WITH_TEXT_ATTRIBUTES)
64
83
  end
65
84
 
66
85
  private
@@ -100,15 +119,31 @@ module CucumberFactory
100
119
  end
101
120
 
102
121
  def parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil)
103
- build_strategy = BuildStrategy.from_prose(raw_model, raw_variant)
122
+ build_strategy, transient_attributes = CucumberFactory::BuildStrategy.from_prose(raw_model, raw_variant)
104
123
  model_class = build_strategy.model_class
124
+ attributes = parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes, transient_attributes)
125
+ record = build_strategy.create_record(attributes)
126
+ remember_record_names(world, record, attributes)
127
+ record
128
+ end
129
+
130
+ def parse_update(world, raw_model, raw_name, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil)
131
+ model_class = CucumberFactory::BuildStrategy.parse_model_class(raw_model)
132
+ attributes = parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes)
133
+ record = resolve_associated_value(world, model_class, model_class, model_class, raw_name)
134
+ CucumberFactory::UpdateStrategy.new(record).assign_attributes(attributes)
135
+ remember_record_names(world, record, attributes)
136
+ record
137
+ end
138
+
139
+ def parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil, transient_attributes = [])
105
140
  attributes = {}
106
141
  if raw_attributes.try(:strip).present?
107
142
  raw_attribute_fragment_regex = /(?:the |and |with |but |,| )+(.*?) (#{VALUE_SCALAR}|#{VALUE_ARRAY}|#{VALUE_LAST_RECORD})/
108
143
  raw_attributes.scan(raw_attribute_fragment_regex).each do |fragment|
109
144
  attribute = attribute_name_from_prose(fragment[0])
110
145
  value = fragment[1]
111
- attributes[attribute] = attribute_value(world, model_class, attribute, value)
146
+ attributes[attribute] = attribute_value(world, model_class, transient_attributes, attribute, value)
112
147
  end
113
148
  unused_raw_attributes = raw_attributes.gsub(raw_attribute_fragment_regex, '')
114
149
  if unused_raw_attributes.present?
@@ -126,49 +161,74 @@ module CucumberFactory
126
161
  # DocString e.g. "first name: Jane\nlast name: Jenny\n"
127
162
  if raw_multiline_attributes.is_a?(String)
128
163
  raw_multiline_attributes.split("\n").each do |fragment|
129
- raw_attribute, value = fragment.split(': ')
164
+ raw_attribute, value = fragment.split(': ', 2)
130
165
  attribute = attribute_name_from_prose(raw_attribute)
131
- value = "\"#{value}\"" unless matches_fully?(value, VALUE_ARRAY)
132
- attributes[attribute] = attribute_value(world, model_class, attribute, value)
166
+ value = "\"#{value}\"" unless matches_fully?(value, /#{VALUE_ARRAY}|#{VALUE_FILE}/)
167
+ attributes[attribute] = attribute_value(world, model_class, transient_attributes, attribute, value)
133
168
  end
134
169
  # DataTable e.g. in raw [["first name", "Jane"], ["last name", "Jenny"]]
135
170
  else
136
171
  raw_multiline_attributes.raw.each do |raw_attribute, value|
137
172
  attribute = attribute_name_from_prose(raw_attribute)
138
- value = "\"#{value}\"" unless matches_fully?(value, VALUE_ARRAY)
139
- attributes[attribute] = attribute_value(world, model_class, attribute, value)
173
+ value = "\"#{value}\"" unless matches_fully?(value, /#{VALUE_ARRAY}|#{VALUE_FILE}/)
174
+ attributes[attribute] = attribute_value(world, model_class, transient_attributes, attribute, value)
140
175
  end
141
176
  end
142
177
  end
143
- record = build_strategy.create_record(attributes)
144
- remember_record_names(world, record, attributes)
145
- record
178
+ attributes
146
179
  end
147
180
 
148
- def attribute_value(world, model_class, attribute, value)
149
- association = model_class.respond_to?(:reflect_on_association) ? model_class.reflect_on_association(attribute) : nil
150
-
151
- if matches_fully?(value, VALUE_ARRAY)
152
- elements_str = unquote(value)
153
- value = elements_str.scan(VALUE_SCALAR).map { |v| attribute_value(world, model_class, attribute, v) }
154
- elsif association.present?
155
- if matches_fully?(value, VALUE_LAST_RECORD)
156
- value = CucumberFactory::Switcher.find_last(association.klass) or raise Error, "There is no last #{attribute}"
157
- elsif matches_fully?(value, VALUE_STRING)
158
- value = unquote(value)
159
- value = get_named_record(world, value) || transform_value(world, value)
160
- elsif matches_fully?(value, VALUE_INTEGER)
161
- value = value.to_s
162
- value = get_named_record(world, value) || transform_value(world, value)
163
- else
164
- raise Error, "Cannot set association #{model_class}##{attribute} to #{value}."
165
- end
181
+ def attribute_value(world, model_class, transient_attributes, attribute, value)
182
+ associated, association_class = resolve_association(attribute, model_class, transient_attributes)
183
+
184
+ value = if matches_fully?(value, VALUE_ARRAY)
185
+ array_values = unquote(value).scan(VALUE_SCALAR)
186
+ array_values.map { |v| attribute_value(world, model_class, transient_attributes, attribute, v) }
187
+ elsif associated
188
+ resolve_associated_value(world, model_class, association_class, attribute, value)
166
189
  else
167
- value = resolve_scalar_value(world, model_class, attribute, value)
190
+ resolve_scalar_value(world, model_class, attribute, value)
168
191
  end
169
192
  value
170
193
  end
171
194
 
195
+ def resolve_association(attribute, model_class, transient_attributes)
196
+ return unless model_class.respond_to?(:reflect_on_association)
197
+
198
+ association = model_class.reflect_on_association(attribute)
199
+ association_class = nil
200
+
201
+ if association
202
+ association_class = association.klass unless association.polymorphic?
203
+ associated = true
204
+ elsif transient_attributes.include?(attribute.to_sym)
205
+ klass_name = attribute.to_s.camelize
206
+ if Object.const_defined?(klass_name)
207
+ association_class = klass_name.constantize
208
+ associated = true
209
+ end
210
+ else
211
+ associated = false
212
+ end
213
+ [associated, association_class]
214
+ end
215
+
216
+ def resolve_associated_value(world, model_class, association_class, attribute, value)
217
+ if matches_fully?(value, VALUE_LAST_RECORD)
218
+ raise(Error, "Cannot set last #{model_class}##{attribute} for polymorphic associations") unless association_class.present?
219
+
220
+ CucumberFactory::Switcher.find_last(association_class) || raise(Error, "There is no last #{attribute}")
221
+ elsif matches_fully?(value, VALUE_STRING)
222
+ value = unquote(value)
223
+ get_named_record(world, value) || transform_value(world, value)
224
+ elsif matches_fully?(value, VALUE_INTEGER)
225
+ value = value.to_s
226
+ get_named_record(world, value) || transform_value(world, value)
227
+ else
228
+ raise Error, "Cannot set association #{model_class}##{attribute} to #{value}."
229
+ end
230
+ end
231
+
172
232
  def resolve_scalar_value(world, model_class, attribute, value)
173
233
  if matches_fully?(value, VALUE_STRING)
174
234
  value = unquote(value)
@@ -177,6 +237,9 @@ module CucumberFactory
177
237
  value = value.to_i
178
238
  elsif matches_fully?(value, VALUE_DECIMAL)
179
239
  value = BigDecimal(value)
240
+ elsif matches_fully?(value, VALUE_FILE)
241
+ path = File.path("./#{file_value_to_path(value)}")
242
+ value = File.new(path)
180
243
  else
181
244
  raise Error, "Cannot set attribute #{model_class}##{attribute} to #{value}."
182
245
  end
@@ -184,11 +247,20 @@ module CucumberFactory
184
247
  end
185
248
 
186
249
  def unquote(string)
250
+ # This method removes quotes or brackets from the start and end from a string
251
+ # Examples: 'single' => single, "double" => double, [1, 2, 3] => 1, 2, 3
187
252
  string[1, string.length - 2]
188
253
  end
189
254
 
255
+ def file_value_to_path(string)
256
+ # file paths are marked with a special keyword and enclosed with quotes.
257
+ # Example: file:"/path/image.png"
258
+ # This will extract the path (/path/image.png) from the text fragment above
259
+ unquote string.sub(/\Afile:/, '')
260
+ end
261
+
190
262
  def full_regexp(partial_regexp)
191
- Regexp.new("\\A" + partial_regexp.source + "\\z", partial_regexp.options)
263
+ Regexp.new('\\A(?:' + partial_regexp.source + ')\\z', partial_regexp.options)
192
264
  end
193
265
 
194
266
  def matches_fully?(string, partial_regexp)
@@ -208,7 +280,7 @@ module CucumberFactory
208
280
  end
209
281
 
210
282
  def attribute_name_from_prose(prose)
211
- prose.downcase.gsub(" ", "_").to_sym
283
+ prose.downcase.gsub(' ', '_').to_sym
212
284
  end
213
285
 
214
286
  def remember_record_names(world, record, attributes)
@@ -0,0 +1,30 @@
1
+ module CucumberFactory
2
+
3
+ class UpdateStrategy
4
+
5
+ def initialize(record)
6
+ @record = record
7
+ end
8
+
9
+ def assign_attributes(attributes)
10
+ active_record_strategy(attributes) ||
11
+ ruby_object_strategy(attributes)
12
+ end
13
+
14
+ private
15
+
16
+ def active_record_strategy(attributes)
17
+ return unless @record.respond_to?(:save!)
18
+
19
+ CucumberFactory::Switcher.assign_attributes(@record, attributes)
20
+ @record.save!
21
+ end
22
+
23
+ def ruby_object_strategy(attributes)
24
+ attributes.each do |name, value|
25
+ @record.send("#{name}=".to_sym, value)
26
+ end
27
+ end
28
+
29
+ end
30
+ end