cucumber_factory 2.0.1 → 2.3.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.
@@ -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