cooklang 1.0.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +35 -0
- data/.gitignore +12 -0
- data/.qlty/.gitignore +7 -0
- data/.qlty/configs/.yamllint.yaml +21 -0
- data/.qlty/qlty.toml +101 -0
- data/.rspec +3 -0
- data/.rubocop.yml +340 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +84 -0
- data/README.md +10 -5
- data/Rakefile +12 -0
- data/cooklang.gemspec +35 -0
- data/lib/cooklang/builders/recipe_builder.rb +64 -0
- data/lib/cooklang/builders/step_builder.rb +76 -0
- data/lib/cooklang/lexer.rb +5 -14
- data/lib/cooklang/parser.rb +24 -653
- data/lib/cooklang/parsers/cookware_parser.rb +133 -0
- data/lib/cooklang/parsers/ingredient_parser.rb +179 -0
- data/lib/cooklang/parsers/timer_parser.rb +135 -0
- data/lib/cooklang/processors/element_parser.rb +45 -0
- data/lib/cooklang/processors/metadata_processor.rb +129 -0
- data/lib/cooklang/processors/step_processor.rb +208 -0
- data/lib/cooklang/processors/token_processor.rb +104 -0
- data/lib/cooklang/recipe.rb +25 -15
- data/lib/cooklang/step.rb +12 -2
- data/lib/cooklang/timer.rb +3 -1
- data/lib/cooklang/token_stream.rb +130 -0
- data/lib/cooklang/version.rb +1 -1
- data/spec/comprehensive_spec.rb +179 -0
- data/spec/cooklang_spec.rb +38 -0
- data/spec/fixtures/canonical.yaml +837 -0
- data/spec/formatters/text_spec.rb +189 -0
- data/spec/integration/canonical_spec.rb +211 -0
- data/spec/lexer_spec.rb +357 -0
- data/spec/models/cookware_spec.rb +116 -0
- data/spec/models/ingredient_spec.rb +192 -0
- data/spec/models/metadata_spec.rb +241 -0
- data/spec/models/note_spec.rb +65 -0
- data/spec/models/recipe_spec.rb +171 -0
- data/spec/models/section_spec.rb +65 -0
- data/spec/models/step_spec.rb +236 -0
- data/spec/models/timer_spec.rb +173 -0
- data/spec/parser_spec.rb +398 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/token_stream_spec.rb +278 -0
- metadata +162 -6
data/spec/lexer_spec.rb
ADDED
@@ -0,0 +1,357 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Cooklang::Lexer do
|
6
|
+
describe "#tokenize" do
|
7
|
+
context "with empty input" do
|
8
|
+
it "returns empty array" do
|
9
|
+
lexer = described_class.new("")
|
10
|
+
tokens = lexer.tokenize
|
11
|
+
expect(tokens).to be_empty
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with plain text" do
|
16
|
+
it "tokenizes simple text" do
|
17
|
+
lexer = described_class.new("Hello world")
|
18
|
+
tokens = lexer.tokenize
|
19
|
+
|
20
|
+
expect(tokens.length).to eq(1)
|
21
|
+
expect(tokens[0].type).to eq(:text)
|
22
|
+
expect(tokens[0].value).to eq("Hello world")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "handles text with spaces" do
|
26
|
+
lexer = described_class.new("Mix the ingredients well")
|
27
|
+
tokens = lexer.tokenize
|
28
|
+
|
29
|
+
expect(tokens.length).to eq(1)
|
30
|
+
expect(tokens[0].type).to eq(:text)
|
31
|
+
expect(tokens[0].value).to eq("Mix the ingredients well")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with ingredient markers" do
|
36
|
+
it "tokenizes simple ingredient" do
|
37
|
+
lexer = described_class.new("@salt")
|
38
|
+
tokens = lexer.tokenize
|
39
|
+
|
40
|
+
expect(tokens.length).to eq(2)
|
41
|
+
expect(tokens[0].type).to eq(:ingredient_marker)
|
42
|
+
expect(tokens[0].value).to eq("@")
|
43
|
+
expect(tokens[1].type).to eq(:text)
|
44
|
+
expect(tokens[1].value).to eq("salt")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "tokenizes ingredient with braces" do
|
48
|
+
lexer = described_class.new("@flour{125%g}")
|
49
|
+
tokens = lexer.tokenize
|
50
|
+
|
51
|
+
expect(tokens.map(&:type)).to eq(%i[
|
52
|
+
ingredient_marker text open_brace text percent text close_brace
|
53
|
+
])
|
54
|
+
expect(tokens.map(&:value)).to eq(["@", "flour", "{", "125", "%", "g", "}"])
|
55
|
+
end
|
56
|
+
|
57
|
+
it "tokenizes ingredient with parentheses" do
|
58
|
+
lexer = described_class.new("@onion(diced)")
|
59
|
+
tokens = lexer.tokenize
|
60
|
+
|
61
|
+
expect(tokens.map(&:type)).to eq(%i[
|
62
|
+
ingredient_marker text open_paren text close_paren
|
63
|
+
])
|
64
|
+
expect(tokens.map(&:value)).to eq(["@", "onion", "(", "diced", ")"])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "with cookware markers" do
|
69
|
+
it "tokenizes simple cookware" do
|
70
|
+
lexer = described_class.new("#pan")
|
71
|
+
tokens = lexer.tokenize
|
72
|
+
|
73
|
+
expect(tokens.length).to eq(2)
|
74
|
+
expect(tokens[0].type).to eq(:cookware_marker)
|
75
|
+
expect(tokens[0].value).to eq("#")
|
76
|
+
expect(tokens[1].type).to eq(:text)
|
77
|
+
expect(tokens[1].value).to eq("pan")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "tokenizes cookware with braces" do
|
81
|
+
lexer = described_class.new("#frying pan{1}")
|
82
|
+
tokens = lexer.tokenize
|
83
|
+
|
84
|
+
expect(tokens.map(&:type)).to eq(%i[
|
85
|
+
cookware_marker text open_brace text close_brace
|
86
|
+
])
|
87
|
+
expect(tokens.map(&:value)).to eq(["#", "frying pan", "{", "1", "}"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "with timer markers" do
|
92
|
+
it "tokenizes simple timer" do
|
93
|
+
lexer = described_class.new("~{10%minutes}")
|
94
|
+
tokens = lexer.tokenize
|
95
|
+
|
96
|
+
expect(tokens.map(&:type)).to eq(%i[
|
97
|
+
timer_marker open_brace text percent text close_brace
|
98
|
+
])
|
99
|
+
expect(tokens.map(&:value)).to eq(["~", "{", "10", "%", "minutes", "}"])
|
100
|
+
end
|
101
|
+
|
102
|
+
it "tokenizes named timer" do
|
103
|
+
lexer = described_class.new("~prep{5%minutes}")
|
104
|
+
tokens = lexer.tokenize
|
105
|
+
|
106
|
+
expect(tokens.map(&:type)).to eq(%i[
|
107
|
+
timer_marker text open_brace text percent text close_brace
|
108
|
+
])
|
109
|
+
expect(tokens.map(&:value)).to eq(["~", "prep", "{", "5", "%", "minutes", "}"])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "with comments" do
|
114
|
+
it "tokenizes line comments" do
|
115
|
+
lexer = described_class.new("-- This is a comment")
|
116
|
+
tokens = lexer.tokenize
|
117
|
+
|
118
|
+
expect(tokens.map(&:type)).to eq(%i[comment_line text])
|
119
|
+
expect(tokens.map(&:value)).to eq(["--", " This is a comment"])
|
120
|
+
end
|
121
|
+
|
122
|
+
it "tokenizes block comments" do
|
123
|
+
lexer = described_class.new("[- This is a block comment -]")
|
124
|
+
tokens = lexer.tokenize
|
125
|
+
|
126
|
+
expect(tokens.map(&:type)).to eq(%i[comment_block_start text comment_block_end])
|
127
|
+
expect(tokens.map(&:value)).to eq(["[-", " This is a block comment ", "-]"])
|
128
|
+
end
|
129
|
+
|
130
|
+
it "handles incomplete block comments" do
|
131
|
+
lexer = described_class.new("[- Incomplete comment")
|
132
|
+
tokens = lexer.tokenize
|
133
|
+
|
134
|
+
expect(tokens.map(&:type)).to eq(%i[comment_block_start text])
|
135
|
+
expect(tokens.map(&:value)).to eq(["[-", " Incomplete comment"])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "with metadata markers" do
|
140
|
+
it "tokenizes metadata marker" do
|
141
|
+
lexer = described_class.new(">> servings: 4")
|
142
|
+
tokens = lexer.tokenize
|
143
|
+
|
144
|
+
expect(tokens.map(&:type)).to eq(%i[metadata_marker text])
|
145
|
+
expect(tokens.map(&:value)).to eq([">>", " servings: 4"])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "with section markers" do
|
150
|
+
it "tokenizes single equals" do
|
151
|
+
lexer = described_class.new("= Dough")
|
152
|
+
tokens = lexer.tokenize
|
153
|
+
|
154
|
+
expect(tokens.map(&:type)).to eq(%i[section_marker text])
|
155
|
+
expect(tokens.map(&:value)).to eq(["=", " Dough"])
|
156
|
+
end
|
157
|
+
|
158
|
+
it "tokenizes double equals" do
|
159
|
+
lexer = described_class.new("== Filling ==")
|
160
|
+
tokens = lexer.tokenize
|
161
|
+
|
162
|
+
expect(tokens.map(&:type)).to eq(%i[section_marker text section_marker])
|
163
|
+
expect(tokens.map(&:value)).to eq(["==", " Filling ", "=="])
|
164
|
+
end
|
165
|
+
|
166
|
+
it "tokenizes multiple equals" do
|
167
|
+
lexer = described_class.new("=== Section ===")
|
168
|
+
tokens = lexer.tokenize
|
169
|
+
|
170
|
+
expect(tokens.map(&:type)).to eq(%i[section_marker text section_marker])
|
171
|
+
expect(tokens.map(&:value)).to eq(["===", " Section ", "==="])
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context "with note markers" do
|
176
|
+
it "tokenizes note marker" do
|
177
|
+
lexer = described_class.new("> Don't burn the roux!")
|
178
|
+
tokens = lexer.tokenize
|
179
|
+
|
180
|
+
expect(tokens.map(&:type)).to eq(%i[note_marker text])
|
181
|
+
expect(tokens.map(&:value)).to eq([">", " Don't burn the roux!"])
|
182
|
+
end
|
183
|
+
|
184
|
+
it "does not tokenize > in metadata marker >>" do
|
185
|
+
lexer = described_class.new(">> servings: 4")
|
186
|
+
tokens = lexer.tokenize
|
187
|
+
|
188
|
+
expect(tokens.map(&:type)).to eq(%i[metadata_marker text])
|
189
|
+
expect(tokens.map(&:value)).to eq([">>", " servings: 4"])
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "with YAML delimiters" do
|
194
|
+
it "tokenizes YAML delimiter" do
|
195
|
+
lexer = described_class.new("---")
|
196
|
+
tokens = lexer.tokenize
|
197
|
+
|
198
|
+
expect(tokens.length).to eq(1)
|
199
|
+
expect(tokens[0].type).to eq(:yaml_delimiter)
|
200
|
+
expect(tokens[0].value).to eq("---")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context "with newlines" do
|
205
|
+
it "tokenizes newlines" do
|
206
|
+
lexer = described_class.new("Line 1\nLine 2")
|
207
|
+
tokens = lexer.tokenize
|
208
|
+
|
209
|
+
expect(tokens.map(&:type)).to eq(%i[text newline text])
|
210
|
+
expect(tokens.map(&:value)).to eq(["Line 1", "\n", "Line 2"])
|
211
|
+
end
|
212
|
+
|
213
|
+
it "tracks line numbers correctly" do
|
214
|
+
lexer = described_class.new("Line 1\nLine 2\nLine 3")
|
215
|
+
tokens = lexer.tokenize
|
216
|
+
|
217
|
+
expect(tokens[0].line).to eq(1) # "Line 1"
|
218
|
+
expect(tokens[1].line).to eq(1) # "\n"
|
219
|
+
expect(tokens[2].line).to eq(2) # "Line 2"
|
220
|
+
expect(tokens[3].line).to eq(2) # "\n"
|
221
|
+
expect(tokens[4].line).to eq(3) # "Line 3"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
context "with complex recipes" do
|
226
|
+
it "tokenizes a complete recipe step" do
|
227
|
+
input = "Mix @flour{125%g} with @milk{250%ml} in a #bowl{1}."
|
228
|
+
lexer = described_class.new(input)
|
229
|
+
tokens = lexer.tokenize
|
230
|
+
|
231
|
+
expected_types = %i[
|
232
|
+
text ingredient_marker text open_brace text percent text close_brace
|
233
|
+
text ingredient_marker text open_brace text percent text close_brace
|
234
|
+
text cookware_marker text open_brace text close_brace text
|
235
|
+
]
|
236
|
+
|
237
|
+
expect(tokens.map(&:type)).to eq(expected_types)
|
238
|
+
end
|
239
|
+
|
240
|
+
it "tokenizes recipe with timer and comments" do
|
241
|
+
input = "Cook for ~{10%minutes} -- until golden\nServe hot"
|
242
|
+
lexer = described_class.new(input)
|
243
|
+
tokens = lexer.tokenize
|
244
|
+
|
245
|
+
expected_values = [
|
246
|
+
"Cook for ", "~", "{", "10", "%", "minutes", "}", " ",
|
247
|
+
"--", " until golden", "\n", "Serve hot"
|
248
|
+
]
|
249
|
+
|
250
|
+
expect(tokens.map(&:value)).to eq(expected_values)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context "with position tracking" do
|
255
|
+
it "tracks position correctly" do
|
256
|
+
lexer = described_class.new("@salt")
|
257
|
+
tokens = lexer.tokenize
|
258
|
+
|
259
|
+
expect(tokens[0].position).to eq(0) # '@' at position 0
|
260
|
+
expect(tokens[1].position).to eq(1) # 'salt' at position 1
|
261
|
+
end
|
262
|
+
|
263
|
+
it "tracks column numbers correctly" do
|
264
|
+
lexer = described_class.new("@salt #pan")
|
265
|
+
tokens = lexer.tokenize
|
266
|
+
|
267
|
+
expect(tokens[0].column).to eq(1) # '@'
|
268
|
+
expect(tokens[1].column).to eq(2) # 'salt '
|
269
|
+
expect(tokens[2].column).to eq(7) # '#'
|
270
|
+
expect(tokens[3].column).to eq(8) # 'pan'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context "with edge cases" do
|
275
|
+
it "handles consecutive special characters" do
|
276
|
+
lexer = described_class.new("@#~{}()")
|
277
|
+
tokens = lexer.tokenize
|
278
|
+
|
279
|
+
expected_types = %i[
|
280
|
+
ingredient_marker cookware_marker timer_marker
|
281
|
+
open_brace close_brace open_paren close_paren
|
282
|
+
]
|
283
|
+
|
284
|
+
expect(tokens.map(&:type)).to eq(expected_types)
|
285
|
+
end
|
286
|
+
|
287
|
+
it "handles text between special characters" do
|
288
|
+
lexer = described_class.new("Heat @oil in #pan for ~{2%min}")
|
289
|
+
tokens = lexer.tokenize
|
290
|
+
|
291
|
+
expect(tokens.filter_map { |t| t.value if t.type == :text }).to eq([
|
292
|
+
"Heat ", "oil in ", "pan for ", "2", "min"
|
293
|
+
])
|
294
|
+
end
|
295
|
+
|
296
|
+
it "handles empty braces and parentheses" do
|
297
|
+
lexer = described_class.new("@ingredient{} #cookware() ~timer{}")
|
298
|
+
tokens = lexer.tokenize
|
299
|
+
|
300
|
+
ingredient_tokens = tokens[0..3]
|
301
|
+
expect(ingredient_tokens.map(&:type)).to eq(%i[
|
302
|
+
ingredient_marker text open_brace close_brace
|
303
|
+
])
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context "with whitespace handling" do
|
308
|
+
it "preserves whitespace in text" do
|
309
|
+
lexer = described_class.new(" Mix well ")
|
310
|
+
tokens = lexer.tokenize
|
311
|
+
|
312
|
+
expect(tokens.length).to eq(1)
|
313
|
+
expect(tokens[0].value).to eq(" Mix well ")
|
314
|
+
end
|
315
|
+
|
316
|
+
it "handles tabs and spaces" do
|
317
|
+
lexer = described_class.new("\tMix @salt ")
|
318
|
+
tokens = lexer.tokenize
|
319
|
+
|
320
|
+
expect(tokens[0].value).to eq("\tMix ")
|
321
|
+
expect(tokens[2].value).to eq("salt ")
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context "with unrecognized characters" do
|
326
|
+
it "handles unrecognized characters gracefully" do
|
327
|
+
# Test the else branch in tokenize that handles unrecognized characters
|
328
|
+
lexer = described_class.new("Mix \x01\x02 salt")
|
329
|
+
tokens = lexer.tokenize
|
330
|
+
|
331
|
+
# Should skip unrecognized characters and continue tokenizing
|
332
|
+
expect(tokens.map(&:type)).to eq(%i[text text])
|
333
|
+
expect(tokens.map(&:value)).to eq(["Mix ", " salt"])
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
describe "Token" do
|
339
|
+
it "creates token with all attributes" do
|
340
|
+
token = Cooklang::Token.new(:text, "hello", 5, 2, 10)
|
341
|
+
|
342
|
+
expect(token.type).to eq(:text)
|
343
|
+
expect(token.value).to eq("hello")
|
344
|
+
expect(token.position).to eq(5)
|
345
|
+
expect(token.line).to eq(2)
|
346
|
+
expect(token.column).to eq(10)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "creates token with default position values" do
|
350
|
+
token = Cooklang::Token.new(:text, "hello")
|
351
|
+
|
352
|
+
expect(token.position).to eq(0)
|
353
|
+
expect(token.line).to eq(1)
|
354
|
+
expect(token.column).to eq(1)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Cooklang::Cookware do
|
6
|
+
describe "#initialize" do
|
7
|
+
it "creates cookware with name and quantity" do
|
8
|
+
cookware = described_class.new(name: "frying pan", quantity: 2)
|
9
|
+
|
10
|
+
expect(cookware.name).to eq("frying pan")
|
11
|
+
expect(cookware.quantity).to eq(2)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "creates cookware with only name" do
|
15
|
+
cookware = described_class.new(name: "bowl")
|
16
|
+
|
17
|
+
expect(cookware.name).to eq("bowl")
|
18
|
+
expect(cookware.quantity).to be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "converts name to string and freezes it" do
|
22
|
+
cookware = described_class.new(name: :pan)
|
23
|
+
|
24
|
+
expect(cookware.name).to eq("pan")
|
25
|
+
expect(cookware.name).to be_frozen
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#to_s" do
|
30
|
+
it "returns name only when no quantity" do
|
31
|
+
cookware = described_class.new(name: "bowl")
|
32
|
+
expect(cookware.to_s).to eq("bowl")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "includes quantity when present" do
|
36
|
+
cookware = described_class.new(name: "frying pan", quantity: 2)
|
37
|
+
expect(cookware.to_s).to eq("frying pan (2)")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#to_h" do
|
42
|
+
it "returns hash with all present attributes" do
|
43
|
+
cookware = described_class.new(name: "frying pan", quantity: 2)
|
44
|
+
|
45
|
+
expected = {
|
46
|
+
name: "frying pan",
|
47
|
+
quantity: 2
|
48
|
+
}
|
49
|
+
|
50
|
+
expect(cookware.to_h).to eq(expected)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "omits nil attributes" do
|
54
|
+
cookware = described_class.new(name: "bowl")
|
55
|
+
|
56
|
+
expect(cookware.to_h).to eq({ name: "bowl" })
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#==" do
|
61
|
+
it "returns true for cookware with same attributes" do
|
62
|
+
cookware1 = described_class.new(name: "pan", quantity: 1)
|
63
|
+
cookware2 = described_class.new(name: "pan", quantity: 1)
|
64
|
+
|
65
|
+
expect(cookware1).to eq(cookware2)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns false for cookware with different names" do
|
69
|
+
cookware1 = described_class.new(name: "pan")
|
70
|
+
cookware2 = described_class.new(name: "bowl")
|
71
|
+
|
72
|
+
expect(cookware1).not_to eq(cookware2)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns false for cookware with different quantities" do
|
76
|
+
cookware1 = described_class.new(name: "pan", quantity: 1)
|
77
|
+
cookware2 = described_class.new(name: "pan", quantity: 2)
|
78
|
+
|
79
|
+
expect(cookware1).not_to eq(cookware2)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns false for non-Cookware objects" do
|
83
|
+
cookware = described_class.new(name: "pan")
|
84
|
+
|
85
|
+
expect(cookware).not_to eq("pan")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#has_quantity?" do
|
90
|
+
it "returns true when quantity is present" do
|
91
|
+
cookware = described_class.new(name: "pan", quantity: 1)
|
92
|
+
expect(cookware).to have_quantity
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns false when quantity is nil" do
|
96
|
+
cookware = described_class.new(name: "pan")
|
97
|
+
expect(cookware).not_to have_quantity
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#hash" do
|
102
|
+
it "generates same hash for equal cookware" do
|
103
|
+
cookware1 = described_class.new(name: "pan", quantity: 1)
|
104
|
+
cookware2 = described_class.new(name: "pan", quantity: 1)
|
105
|
+
|
106
|
+
expect(cookware1.hash).to eq(cookware2.hash)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "generates different hash for different cookware" do
|
110
|
+
cookware1 = described_class.new(name: "pan")
|
111
|
+
cookware2 = described_class.new(name: "bowl")
|
112
|
+
|
113
|
+
expect(cookware1.hash).not_to eq(cookware2.hash)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Cooklang::Ingredient do
|
6
|
+
describe "#initialize" do
|
7
|
+
it "creates an ingredient with all attributes" do
|
8
|
+
ingredient = described_class.new(
|
9
|
+
name: "flour",
|
10
|
+
quantity: 125,
|
11
|
+
unit: "g",
|
12
|
+
notes: "sifted"
|
13
|
+
)
|
14
|
+
|
15
|
+
expect(ingredient.name).to eq("flour")
|
16
|
+
expect(ingredient.quantity).to eq(125)
|
17
|
+
expect(ingredient.unit).to eq("g")
|
18
|
+
expect(ingredient.notes).to eq("sifted")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates an ingredient with only name" do
|
22
|
+
ingredient = described_class.new(name: "salt")
|
23
|
+
|
24
|
+
expect(ingredient.name).to eq("salt")
|
25
|
+
expect(ingredient.quantity).to be_nil
|
26
|
+
expect(ingredient.unit).to be_nil
|
27
|
+
expect(ingredient.notes).to be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it "converts name to string and freezes it" do
|
31
|
+
ingredient = described_class.new(name: :flour)
|
32
|
+
|
33
|
+
expect(ingredient.name).to eq("flour")
|
34
|
+
expect(ingredient.name).to be_frozen
|
35
|
+
end
|
36
|
+
|
37
|
+
it "converts unit to string and freezes it" do
|
38
|
+
ingredient = described_class.new(name: "flour", unit: :grams)
|
39
|
+
|
40
|
+
expect(ingredient.unit).to eq("grams")
|
41
|
+
expect(ingredient.unit).to be_frozen
|
42
|
+
end
|
43
|
+
|
44
|
+
it "converts notes to string and freezes it" do
|
45
|
+
ingredient = described_class.new(name: "flour", notes: :sifted)
|
46
|
+
|
47
|
+
expect(ingredient.notes).to eq("sifted")
|
48
|
+
expect(ingredient.notes).to be_frozen
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#to_s" do
|
53
|
+
it "returns name only when no other attributes" do
|
54
|
+
ingredient = described_class.new(name: "salt")
|
55
|
+
expect(ingredient.to_s).to eq("salt")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "includes quantity when present" do
|
59
|
+
ingredient = described_class.new(name: "flour", quantity: 125)
|
60
|
+
expect(ingredient.to_s).to eq("flour 125")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "includes unit when present" do
|
64
|
+
ingredient = described_class.new(name: "flour", quantity: 125, unit: "g")
|
65
|
+
expect(ingredient.to_s).to eq("flour 125 g")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "includes notes when present" do
|
69
|
+
ingredient = described_class.new(name: "flour", notes: "sifted")
|
70
|
+
expect(ingredient.to_s).to eq("flour (sifted)")
|
71
|
+
end
|
72
|
+
|
73
|
+
it "includes all attributes when present" do
|
74
|
+
ingredient = described_class.new(
|
75
|
+
name: "flour",
|
76
|
+
quantity: 125,
|
77
|
+
unit: "g",
|
78
|
+
notes: "sifted"
|
79
|
+
)
|
80
|
+
expect(ingredient.to_s).to eq("flour 125 g (sifted)")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#to_h" do
|
85
|
+
it "returns hash with all present attributes" do
|
86
|
+
ingredient = described_class.new(
|
87
|
+
name: "flour",
|
88
|
+
quantity: 125,
|
89
|
+
unit: "g",
|
90
|
+
notes: "sifted"
|
91
|
+
)
|
92
|
+
|
93
|
+
expected = {
|
94
|
+
name: "flour",
|
95
|
+
quantity: 125,
|
96
|
+
unit: "g",
|
97
|
+
notes: "sifted"
|
98
|
+
}
|
99
|
+
|
100
|
+
expect(ingredient.to_h).to eq(expected)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "omits nil attributes" do
|
104
|
+
ingredient = described_class.new(name: "salt")
|
105
|
+
|
106
|
+
expect(ingredient.to_h).to eq({ name: "salt" })
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#==" do
|
111
|
+
it "returns true for ingredients with same attributes" do
|
112
|
+
ingredient1 = described_class.new(name: "flour", quantity: 125, unit: "g")
|
113
|
+
ingredient2 = described_class.new(name: "flour", quantity: 125, unit: "g")
|
114
|
+
|
115
|
+
expect(ingredient1).to eq(ingredient2)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "returns false for ingredients with different names" do
|
119
|
+
ingredient1 = described_class.new(name: "flour")
|
120
|
+
ingredient2 = described_class.new(name: "salt")
|
121
|
+
|
122
|
+
expect(ingredient1).not_to eq(ingredient2)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "returns false for ingredients with different quantities" do
|
126
|
+
ingredient1 = described_class.new(name: "flour", quantity: 125)
|
127
|
+
ingredient2 = described_class.new(name: "flour", quantity: 100)
|
128
|
+
|
129
|
+
expect(ingredient1).not_to eq(ingredient2)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "returns false for non-Ingredient objects" do
|
133
|
+
ingredient = described_class.new(name: "flour")
|
134
|
+
|
135
|
+
expect(ingredient).not_to eq("flour")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "predicate methods" do
|
140
|
+
describe "#has_quantity?" do
|
141
|
+
it "returns true when quantity is present" do
|
142
|
+
ingredient = described_class.new(name: "flour", quantity: 125)
|
143
|
+
expect(ingredient).to have_quantity
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns false when quantity is nil" do
|
147
|
+
ingredient = described_class.new(name: "flour")
|
148
|
+
expect(ingredient).not_to have_quantity
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#has_unit?" do
|
153
|
+
it "returns true when unit is present" do
|
154
|
+
ingredient = described_class.new(name: "flour", unit: "g")
|
155
|
+
expect(ingredient).to have_unit
|
156
|
+
end
|
157
|
+
|
158
|
+
it "returns false when unit is nil" do
|
159
|
+
ingredient = described_class.new(name: "flour")
|
160
|
+
expect(ingredient).not_to have_unit
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#has_notes?" do
|
165
|
+
it "returns true when notes are present" do
|
166
|
+
ingredient = described_class.new(name: "flour", notes: "sifted")
|
167
|
+
expect(ingredient).to have_notes
|
168
|
+
end
|
169
|
+
|
170
|
+
it "returns false when notes are nil" do
|
171
|
+
ingredient = described_class.new(name: "flour")
|
172
|
+
expect(ingredient).not_to have_notes
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "#hash" do
|
178
|
+
it "generates same hash for equal ingredients" do
|
179
|
+
ingredient1 = described_class.new(name: "flour", quantity: 125)
|
180
|
+
ingredient2 = described_class.new(name: "flour", quantity: 125)
|
181
|
+
|
182
|
+
expect(ingredient1.hash).to eq(ingredient2.hash)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "generates different hash for different ingredients" do
|
186
|
+
ingredient1 = described_class.new(name: "flour")
|
187
|
+
ingredient2 = described_class.new(name: "salt")
|
188
|
+
|
189
|
+
expect(ingredient1.hash).not_to eq(ingredient2.hash)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|