cucumber_factory 2.1.0 → 2.4.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.
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Runtime dependencies
4
+ gem 'cucumber', '~> 5.3.0'
5
+ gem 'activesupport', '~> 6.1.0'
6
+ gem 'activerecord', '~> 6.1.0'
7
+ gem 'pg'
8
+
9
+ # Development dependencies
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'rake'
12
+ gem 'database_cleaner'
13
+ gem 'gemika'
14
+
15
+ # Test dependencies
16
+ gem 'factory_bot'
17
+ gem 'carrierwave'
18
+
19
+ # Gem under test
20
+ gem 'cucumber_factory', :path => '.'
@@ -0,0 +1,140 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cucumber_factory (2.4.0)
5
+ activerecord
6
+ activesupport
7
+ cucumber
8
+ cucumber_priority (>= 0.2.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activemodel (6.1.3.1)
14
+ activesupport (= 6.1.3.1)
15
+ activerecord (6.1.3.1)
16
+ activemodel (= 6.1.3.1)
17
+ activesupport (= 6.1.3.1)
18
+ activesupport (6.1.3.1)
19
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ i18n (>= 1.6, < 2)
21
+ minitest (>= 5.1)
22
+ tzinfo (~> 2.0)
23
+ zeitwerk (~> 2.3)
24
+ addressable (2.7.0)
25
+ public_suffix (>= 2.0.2, < 5.0)
26
+ builder (3.2.4)
27
+ carrierwave (2.2.1)
28
+ activemodel (>= 5.0.0)
29
+ activesupport (>= 5.0.0)
30
+ addressable (~> 2.6)
31
+ image_processing (~> 1.1)
32
+ marcel (~> 1.0.0)
33
+ mini_mime (>= 0.1.3)
34
+ ssrf_filter (~> 1.0)
35
+ concurrent-ruby (1.1.8)
36
+ cucumber (5.3.0)
37
+ builder (~> 3.2, >= 3.2.4)
38
+ cucumber-core (~> 8.0, >= 8.0.1)
39
+ cucumber-create-meta (~> 2.0, >= 2.0.2)
40
+ cucumber-cucumber-expressions (~> 10.3, >= 10.3.0)
41
+ cucumber-gherkin (~> 15.0, >= 15.0.2)
42
+ cucumber-html-formatter (~> 9.0, >= 9.0.0)
43
+ cucumber-messages (~> 13.1, >= 13.1.0)
44
+ cucumber-wire (~> 4.0, >= 4.0.1)
45
+ diff-lcs (~> 1.4, >= 1.4.4)
46
+ multi_test (~> 0.1, >= 0.1.2)
47
+ sys-uname (~> 1.2, >= 1.2.1)
48
+ cucumber-core (8.0.1)
49
+ cucumber-gherkin (~> 15.0, >= 15.0.2)
50
+ cucumber-messages (~> 13.0, >= 13.0.1)
51
+ cucumber-tag-expressions (~> 2.0, >= 2.0.4)
52
+ cucumber-create-meta (2.0.4)
53
+ cucumber-messages (~> 13.1, >= 13.1.0)
54
+ sys-uname (~> 1.2, >= 1.2.1)
55
+ cucumber-cucumber-expressions (10.3.0)
56
+ cucumber-gherkin (15.0.2)
57
+ cucumber-messages (~> 13.0, >= 13.0.1)
58
+ cucumber-html-formatter (9.0.0)
59
+ cucumber-messages (~> 13.0, >= 13.0.1)
60
+ cucumber-messages (13.2.1)
61
+ protobuf-cucumber (~> 3.10, >= 3.10.8)
62
+ cucumber-tag-expressions (2.0.4)
63
+ cucumber-wire (4.0.1)
64
+ cucumber-core (~> 8.0, >= 8.0.1)
65
+ cucumber-cucumber-expressions (~> 10.3, >= 10.3.0)
66
+ cucumber-messages (~> 13.0, >= 13.0.1)
67
+ cucumber_priority (0.3.2)
68
+ cucumber
69
+ database_cleaner (2.0.1)
70
+ database_cleaner-active_record (~> 2.0.0)
71
+ database_cleaner-active_record (2.0.0)
72
+ activerecord (>= 5.a)
73
+ database_cleaner-core (~> 2.0.0)
74
+ database_cleaner-core (2.0.1)
75
+ diff-lcs (1.4.4)
76
+ factory_bot (6.1.0)
77
+ activesupport (>= 5.0.0)
78
+ ffi (1.15.0)
79
+ gemika (0.5.0)
80
+ i18n (1.8.9)
81
+ concurrent-ruby (~> 1.0)
82
+ image_processing (1.12.1)
83
+ mini_magick (>= 4.9.5, < 5)
84
+ ruby-vips (>= 2.0.17, < 3)
85
+ marcel (1.0.0)
86
+ middleware (0.1.0)
87
+ mini_magick (4.11.0)
88
+ mini_mime (1.0.3)
89
+ minitest (5.14.4)
90
+ multi_test (0.1.2)
91
+ pg (1.2.3)
92
+ protobuf-cucumber (3.10.8)
93
+ activesupport (>= 3.2)
94
+ middleware
95
+ thor
96
+ thread_safe
97
+ public_suffix (4.0.6)
98
+ rake (13.0.3)
99
+ rspec (3.10.0)
100
+ rspec-core (~> 3.10.0)
101
+ rspec-expectations (~> 3.10.0)
102
+ rspec-mocks (~> 3.10.0)
103
+ rspec-core (3.10.1)
104
+ rspec-support (~> 3.10.0)
105
+ rspec-expectations (3.10.1)
106
+ diff-lcs (>= 1.2.0, < 2.0)
107
+ rspec-support (~> 3.10.0)
108
+ rspec-mocks (3.10.2)
109
+ diff-lcs (>= 1.2.0, < 2.0)
110
+ rspec-support (~> 3.10.0)
111
+ rspec-support (3.10.2)
112
+ ruby-vips (2.1.0)
113
+ ffi (~> 1.12)
114
+ ssrf_filter (1.0.7)
115
+ sys-uname (1.2.2)
116
+ ffi (~> 1.1)
117
+ thor (1.1.0)
118
+ thread_safe (0.3.6)
119
+ tzinfo (2.0.4)
120
+ concurrent-ruby (~> 1.0)
121
+ zeitwerk (2.4.2)
122
+
123
+ PLATFORMS
124
+ ruby
125
+
126
+ DEPENDENCIES
127
+ activerecord (~> 6.1.0)
128
+ activesupport (~> 6.1.0)
129
+ carrierwave
130
+ cucumber (~> 5.3.0)
131
+ cucumber_factory!
132
+ database_cleaner
133
+ factory_bot
134
+ gemika
135
+ pg
136
+ rake
137
+ rspec (~> 3.0)
138
+
139
+ BUNDLED WITH
140
+ 2.1.4
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- cucumber_factory [![Build Status](https://travis-ci.org/makandra/cucumber_factory.svg?branch=master)](https://travis-ci.org/makandra/cucumber_factory)
1
+ cucumber_factory [![Tests](https://github.com/makandra/cucumber_factory/workflows/Tests/badge.svg)](https://github.com/makandra/cucumber_factory/actions?query=branch:master)
2
2
  ================
3
3
 
4
4
  Create ActiveRecord objects without step definitions
@@ -24,6 +24,13 @@ Quoted strings and numbers denote attribute values:
24
24
  Given there is a movie with the title "Sunshine" and the year 2007
25
25
  ```
26
26
 
27
+ To update an existing record, specify the record and the changes:
28
+ ```
29
+ Given the movie above has the title "Sunset" and the year 2008
30
+ Given the movie "Sunrise" has the year 2009
31
+ ```
32
+ A record can be specified by the `above` keyword, which uses the last created record of this class, or by any string that was used during its creation.
33
+
27
34
  Setting boolean attributes
28
35
  --------------------------
29
36
 
@@ -42,6 +49,13 @@ Given there is a movie which is awesome, popular and successful but not science
42
49
  And there is a director with the income "500000" but with the account balance "-30000"
43
50
  ```
44
51
 
52
+ To update boolean attributes use the keyword `is`:
53
+
54
+ ```cucumber
55
+ Given the movie above is awesome but not popular
56
+ Given the movie above has the year 1979 but is not science fiction
57
+ ```
58
+
45
59
 
46
60
  Setting many attributes with a table
47
61
  ------------------------------------
@@ -62,6 +76,14 @@ Given there is a movie with these attributes:
62
76
  | comedy | false |
63
77
  ```
64
78
 
79
+ ```cucumber
80
+ Given the movie above has these attributes:
81
+ """
82
+ name: Sunshine
83
+ comedy: false
84
+ """
85
+ ```
86
+
65
87
  Setting associations
66
88
  --------------------
67
89
 
@@ -129,7 +151,16 @@ Given there is a movie with the tags ["comedy", "drama" and "action"]
129
151
  ```
130
152
 
131
153
 
154
+ Setting file attributes
155
+ -----------------------
156
+
157
+ You can set an attribute to a file object with the following syntax:
158
+
159
+ ```cucumber
160
+ Given there is a movie with the image file:'path/to/image.jpg'
161
+ ```
132
162
 
163
+ All paths are relative to the project root, absolute paths are not supported. Please note that file attributes must follow the syntax `file:"PATH"`, both single and double quotes are allowed.
133
164
 
134
165
  Using named factories and traits
135
166
  --------------------------------
@@ -191,23 +222,22 @@ Development
191
222
  There are tests in `spec`. We only accept PRs with tests. To run tests:
192
223
 
193
224
  - Install the Ruby version stated in `.ruby-version`
194
- - Create a local MySQL/MariaDB database named `cucumber_factory_test`
225
+ - Create a local PostgreSQL database:
195
226
  ```
196
- $ mysql -u root -p
197
- > create database cucumber_factory_test;
227
+ $ sudo -u postgres psql -c 'create database cucumber_factory_test;'
198
228
  ```
199
229
 
200
230
  - Copy `spec/support/database.sample.yml` to `spec/support/database.yml` and enter your local credentials for the test databases
201
231
  - Install development dependencies using `bundle install`
202
232
  - Run tests with the default symlinked Gemfile using `bundle exec rspec` or explicit with `BUNDLE_GEMFILE=Gemfile.cucumber-x.x bundle exec rspec spec`
203
233
 
204
- We recommend to test large changes against multiple versions of Ruby and multiple dependency sets. Supported combinations are configured in `.travis.yml`. We provide some rake tasks to help with this:
234
+ We recommend to test large changes against multiple versions of Ruby and multiple dependency sets. Supported combinations are configured in .github/workflows/test.yml. We provide some rake tasks to help with this:
205
235
 
206
236
  For each ruby version do (you need to change it manually):
207
237
  - Install development dependencies using `rake matrix:install`
208
238
  - Run tests using `rake matrix:spec`
209
239
 
210
- Note that we have configured Travis CI to automatically run tests in all supported Ruby versions and dependency sets after each push. We will only merge pull requests after a green Travis build.
240
+ Note that we have configured GitHub Actions to automatically run tests in all supported Ruby versions and dependency sets after each push. We will only merge pull requests after a green workflow build.
211
241
 
212
242
  If you would like to contribute:
213
243
 
@@ -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
 
@@ -11,6 +11,7 @@ module CucumberFactory
11
11
  factory = factory_bot_factory(model_prose, variants)
12
12
 
13
13
  if factory
14
+ factory.compile # Required to load inherited traits!
14
15
  strategy = factory_bot_strategy(factory, model_prose, variants)
15
16
  transient_attributes = factory_bot_transient_attributes(factory, variants)
16
17
  else
@@ -21,6 +22,10 @@ module CucumberFactory
21
22
  [strategy, transient_attributes]
22
23
  end
23
24
 
25
+ def parse_model_class(model_prose)
26
+ underscored_model_name(model_prose).camelize.constantize
27
+ end
28
+
24
29
  private
25
30
 
26
31
  def variants_from_prose(variant_prose)
@@ -84,7 +89,7 @@ module CucumberFactory
84
89
  end
85
90
 
86
91
  def alternative_strategy(model_prose, variants)
87
- model_class = underscored_model_name(model_prose).camelize.constantize
92
+ model_class = parse_model_class(model_prose)
88
93
  machinist_strategy(model_class, variants) ||
89
94
  active_record_strategy(model_class) ||
90
95
  ruby_object_strategy(model_class)
@@ -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
@@ -69,7 +88,8 @@ module CucumberFactory
69
88
  main.instance_eval {
70
89
  kind = descriptor[:kind]
71
90
  object = send(kind, *[descriptor[:pattern]].compact, &descriptor[:block])
72
- object.overridable(:priority => descriptor[:priority] ? 1 : 0) if kind != :Before
91
+ # cucumber_factory steps get a low priority due to their generic syntax
92
+ object.overridable(:priority => descriptor[:priority] ? -1 : -2) if kind != :Before
73
93
  object
74
94
  }
75
95
  end
@@ -100,8 +120,24 @@ module CucumberFactory
100
120
  end
101
121
 
102
122
  def parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil)
103
- build_strategy, transient_attributes = BuildStrategy.from_prose(raw_model, raw_variant)
123
+ build_strategy, transient_attributes = CucumberFactory::BuildStrategy.from_prose(raw_model, raw_variant)
104
124
  model_class = build_strategy.model_class
125
+ attributes = parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes, transient_attributes)
126
+ record = build_strategy.create_record(attributes)
127
+ remember_record_names(world, record, attributes)
128
+ record
129
+ end
130
+
131
+ def parse_update(world, raw_model, raw_name, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil)
132
+ model_class = CucumberFactory::BuildStrategy.parse_model_class(raw_model)
133
+ attributes = parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes)
134
+ record = resolve_associated_value(world, model_class, model_class, model_class, raw_name)
135
+ CucumberFactory::UpdateStrategy.new(record).assign_attributes(attributes)
136
+ remember_record_names(world, record, attributes)
137
+ record
138
+ end
139
+
140
+ def parse_attributes(world, model_class, raw_attributes, raw_boolean_attributes, raw_multiline_attributes = nil, transient_attributes = [])
105
141
  attributes = {}
106
142
  if raw_attributes.try(:strip).present?
107
143
  raw_attribute_fragment_regex = /(?:the |and |with |but |,| )+(.*?) (#{VALUE_SCALAR}|#{VALUE_ARRAY}|#{VALUE_LAST_RECORD})/
@@ -126,59 +162,71 @@ module CucumberFactory
126
162
  # DocString e.g. "first name: Jane\nlast name: Jenny\n"
127
163
  if raw_multiline_attributes.is_a?(String)
128
164
  raw_multiline_attributes.split("\n").each do |fragment|
129
- raw_attribute, value = fragment.split(': ')
165
+ raw_attribute, value = fragment.split(': ', 2)
130
166
  attribute = attribute_name_from_prose(raw_attribute)
131
- value = "\"#{value}\"" unless matches_fully?(value, VALUE_ARRAY)
167
+ value = "\"#{value}\"" unless matches_fully?(value, /#{VALUE_ARRAY}|#{VALUE_FILE}/)
132
168
  attributes[attribute] = attribute_value(world, model_class, transient_attributes, attribute, value)
133
169
  end
134
170
  # DataTable e.g. in raw [["first name", "Jane"], ["last name", "Jenny"]]
135
171
  else
136
172
  raw_multiline_attributes.raw.each do |raw_attribute, value|
137
173
  attribute = attribute_name_from_prose(raw_attribute)
138
- value = "\"#{value}\"" unless matches_fully?(value, VALUE_ARRAY)
174
+ value = "\"#{value}\"" unless matches_fully?(value, /#{VALUE_ARRAY}|#{VALUE_FILE}/)
139
175
  attributes[attribute] = attribute_value(world, model_class, transient_attributes, attribute, value)
140
176
  end
141
177
  end
142
178
  end
143
- record = build_strategy.create_record(attributes)
144
- remember_record_names(world, record, attributes)
145
- record
179
+ attributes
146
180
  end
147
181
 
148
182
  def attribute_value(world, model_class, transient_attributes, attribute, value)
149
- association_class = resolve_association_class(attribute, model_class, transient_attributes)
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, transient_attributes, attribute, v) }
154
- elsif association_class.present?
155
- if matches_fully?(value, VALUE_LAST_RECORD)
156
- value = CucumberFactory::Switcher.find_last(association_class) 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
183
+ associated, association_class = resolve_association(attribute, model_class, transient_attributes)
184
+
185
+ value = if matches_fully?(value, VALUE_ARRAY)
186
+ array_values = unquote(value).scan(VALUE_SCALAR)
187
+ array_values.map { |v| attribute_value(world, model_class, transient_attributes, attribute, v) }
188
+ elsif associated
189
+ resolve_associated_value(world, model_class, association_class, attribute, value)
166
190
  else
167
- value = resolve_scalar_value(world, model_class, attribute, value)
191
+ resolve_scalar_value(world, model_class, attribute, value)
168
192
  end
169
193
  value
170
194
  end
171
195
 
172
- def resolve_association_class(attribute, model_class, transient_attributes)
196
+ def resolve_association(attribute, model_class, transient_attributes)
173
197
  return unless model_class.respond_to?(:reflect_on_association)
174
198
 
175
- klass = if model_class.reflect_on_association(attribute)
176
- model_class.reflect_on_association(attribute).klass
199
+ association = model_class.reflect_on_association(attribute)
200
+ association_class = nil
201
+
202
+ if association
203
+ association_class = association.klass unless association.polymorphic?
204
+ associated = true
177
205
  elsif transient_attributes.include?(attribute.to_sym)
178
206
  klass_name = attribute.to_s.camelize
179
- klass_name.constantize if Object.const_defined?(klass_name)
207
+ if Object.const_defined?(klass_name)
208
+ association_class = klass_name.constantize
209
+ associated = true
210
+ end
180
211
  else
181
- nil
212
+ associated = false
213
+ end
214
+ [associated, association_class]
215
+ end
216
+
217
+ def resolve_associated_value(world, model_class, association_class, attribute, value)
218
+ if matches_fully?(value, VALUE_LAST_RECORD)
219
+ raise(Error, "Cannot set last #{model_class}##{attribute} for polymorphic associations") unless association_class.present?
220
+
221
+ CucumberFactory::Switcher.find_last(association_class) || raise(Error, "There is no last #{attribute}")
222
+ elsif matches_fully?(value, VALUE_STRING)
223
+ value = unquote(value)
224
+ get_named_record(world, value) || transform_value(world, value)
225
+ elsif matches_fully?(value, VALUE_INTEGER)
226
+ value = value.to_s
227
+ get_named_record(world, value) || transform_value(world, value)
228
+ else
229
+ raise Error, "Cannot set association #{model_class}##{attribute} to #{value}."
182
230
  end
183
231
  end
184
232
 
@@ -190,6 +238,9 @@ module CucumberFactory
190
238
  value = value.to_i
191
239
  elsif matches_fully?(value, VALUE_DECIMAL)
192
240
  value = BigDecimal(value)
241
+ elsif matches_fully?(value, VALUE_FILE)
242
+ path = File.path("./#{file_value_to_path(value)}")
243
+ value = File.new(path)
193
244
  else
194
245
  raise Error, "Cannot set attribute #{model_class}##{attribute} to #{value}."
195
246
  end
@@ -197,11 +248,20 @@ module CucumberFactory
197
248
  end
198
249
 
199
250
  def unquote(string)
251
+ # This method removes quotes or brackets from the start and end from a string
252
+ # Examples: 'single' => single, "double" => double, [1, 2, 3] => 1, 2, 3
200
253
  string[1, string.length - 2]
201
254
  end
202
255
 
256
+ def file_value_to_path(string)
257
+ # file paths are marked with a special keyword and enclosed with quotes.
258
+ # Example: file:"/path/image.png"
259
+ # This will extract the path (/path/image.png) from the text fragment above
260
+ unquote string.sub(/\Afile:/, '')
261
+ end
262
+
203
263
  def full_regexp(partial_regexp)
204
- Regexp.new("\\A" + partial_regexp.source + "\\z", partial_regexp.options)
264
+ Regexp.new('\\A(?:' + partial_regexp.source + ')\\z', partial_regexp.options)
205
265
  end
206
266
 
207
267
  def matches_fully?(string, partial_regexp)
@@ -221,7 +281,7 @@ module CucumberFactory
221
281
  end
222
282
 
223
283
  def attribute_name_from_prose(prose)
224
- prose.downcase.gsub(" ", "_").to_sym
284
+ prose.downcase.gsub(' ', '_').to_sym
225
285
  end
226
286
 
227
287
  def remember_record_names(world, record, attributes)