cooklang 1.0.2 → 1.0.4
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 +2 -2
- data/.gitignore +1 -0
- data/README.md +26 -7
- data/cooklang.gemspec +2 -2
- data/lib/cooklang/formatter.rb +0 -46
- data/lib/cooklang/formatters/hash.rb +1 -1
- data/lib/cooklang/formatters/text.rb +46 -0
- data/lib/cooklang/lexer.rb +49 -0
- data/lib/cooklang/metadata.rb +2 -66
- data/lib/cooklang/processors/metadata_processor.rb +15 -35
- data/lib/cooklang/version.rb +1 -1
- data/spec/fixtures/comprehensive_recipe.cook +51 -0
- data/spec/formatters/formatter_spec.rb +29 -0
- data/spec/formatters/hash_spec.rb +63 -0
- data/spec/formatters/json_spec.rb +55 -0
- data/spec/formatters/text_spec.rb +30 -168
- data/spec/integration/metadata_canonical_spec.rb +169 -0
- data/spec/models/metadata_spec.rb +19 -149
- data/spec/models/recipe_spec.rb +2 -2
- metadata +16 -7
- data/Gemfile.lock +0 -84
@@ -3,187 +3,49 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
5
|
RSpec.describe Cooklang::Formatters::Text do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
cookware: [],
|
11
|
-
timers: [],
|
12
|
-
metadata: {},
|
13
|
-
sections: [],
|
14
|
-
notes: []
|
15
|
-
)
|
16
|
-
end
|
17
|
-
let(:formatter) { described_class.new(recipe) }
|
18
|
-
|
19
|
-
describe "#to_s" do
|
20
|
-
context "with ingredients only" do
|
21
|
-
let(:ingredients) do
|
22
|
-
[
|
23
|
-
Cooklang::Ingredient.new(name: "flour", quantity: 125, unit: "g"),
|
24
|
-
Cooklang::Ingredient.new(name: "milk", quantity: 250, unit: "ml"),
|
25
|
-
Cooklang::Ingredient.new(name: "eggs", quantity: 3),
|
26
|
-
Cooklang::Ingredient.new(name: "butter"),
|
27
|
-
Cooklang::Ingredient.new(name: "sea salt", quantity: 1, unit: "pinch")
|
28
|
-
]
|
29
|
-
end
|
30
|
-
let(:steps) { [] }
|
31
|
-
|
32
|
-
it "formats ingredients with aligned columns" do
|
33
|
-
expected = <<~OUTPUT.strip
|
34
|
-
Ingredients:
|
35
|
-
flour 125 g
|
36
|
-
milk 250 ml
|
37
|
-
eggs 3
|
38
|
-
butter some
|
39
|
-
sea salt 1 pinch
|
40
|
-
OUTPUT
|
41
|
-
|
42
|
-
expect(formatter.to_s).to eq(expected)
|
43
|
-
end
|
6
|
+
describe "#generate" do
|
7
|
+
let(:recipe) do
|
8
|
+
recipe_text = File.read(File.join(__dir__, "..", "fixtures", "comprehensive_recipe.cook"))
|
9
|
+
Cooklang.parse(recipe_text)
|
44
10
|
end
|
11
|
+
let(:output) { described_class.new(recipe).generate }
|
45
12
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
[
|
50
|
-
Cooklang::Step.new(segments: [
|
51
|
-
{ type: "text", value: "Crack the " },
|
52
|
-
{ type: "ingredient", value: "eggs", name: "eggs" },
|
53
|
-
{ type: "text", value: " into a blender" }
|
54
|
-
]),
|
55
|
-
Cooklang::Step.new(segments: [
|
56
|
-
{ type: "text", value: "Pour into a bowl and leave to stand for " },
|
57
|
-
{ type: "timer", value: "15 minutes", name: nil },
|
58
|
-
{ type: "text", value: "." }
|
59
|
-
])
|
60
|
-
]
|
61
|
-
end
|
62
|
-
|
63
|
-
it "formats steps with numbered list" do
|
64
|
-
expected = <<~OUTPUT.strip
|
65
|
-
Steps:
|
66
|
-
1. Crack the eggs into a blender
|
67
|
-
2. Pour into a bowl and leave to stand for 15 minutes.
|
68
|
-
OUTPUT
|
69
|
-
|
70
|
-
expect(formatter.to_s).to eq(expected)
|
71
|
-
end
|
13
|
+
it "includes Ingredients and Steps sections" do
|
14
|
+
expect(output).to include("Ingredients:")
|
15
|
+
expect(output).to include("Steps:")
|
72
16
|
end
|
73
17
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
Cooklang::Ingredient.new(name: "flour", quantity: 125, unit: "g"),
|
80
|
-
Cooklang::Ingredient.new(name: "milk", quantity: 250, unit: "ml"),
|
81
|
-
Cooklang::Ingredient.new(name: "sea salt", quantity: 1, unit: "pinch")
|
82
|
-
]
|
83
|
-
end
|
84
|
-
let(:steps) do
|
85
|
-
[
|
86
|
-
Cooklang::Step.new(segments: [
|
87
|
-
{ type: "text", value: "Crack the " },
|
88
|
-
{ type: "ingredient", value: "eggs", name: "eggs" },
|
89
|
-
{ type: "text", value: " into a blender, then add the " },
|
90
|
-
{ type: "ingredient", value: "flour", name: "flour" },
|
91
|
-
{ type: "text", value: ", " },
|
92
|
-
{ type: "ingredient", value: "milk", name: "milk" },
|
93
|
-
{ type: "text", value: " and " },
|
94
|
-
{ type: "ingredient", value: "sea salt", name: "sea salt" },
|
95
|
-
{ type: "text", value: "." }
|
96
|
-
]),
|
97
|
-
Cooklang::Step.new(segments: [
|
98
|
-
{ type: "text", value: "Pour into a bowl and leave to stand for " },
|
99
|
-
{ type: "timer", value: "15 minutes", name: nil },
|
100
|
-
{ type: "text", value: "." }
|
101
|
-
]),
|
102
|
-
Cooklang::Step.new(segments: [
|
103
|
-
{ type: "text", value: "Melt the " },
|
104
|
-
{ type: "ingredient", value: "butter", name: "butter" },
|
105
|
-
{ type: "text", value: " in a large non-stick " },
|
106
|
-
{ type: "cookware", value: "frying pan", name: "frying pan" },
|
107
|
-
{ type: "text", value: "." }
|
108
|
-
])
|
109
|
-
]
|
110
|
-
end
|
111
|
-
|
112
|
-
it "formats complete recipe" do
|
113
|
-
expected = <<~OUTPUT.strip
|
114
|
-
Ingredients:
|
115
|
-
butter some
|
116
|
-
eggs 3
|
117
|
-
flour 125 g
|
118
|
-
milk 250 ml
|
119
|
-
sea salt 1 pinch
|
120
|
-
|
121
|
-
Steps:
|
122
|
-
1. Crack the eggs into a blender, then add the flour, milk and sea salt.
|
123
|
-
2. Pour into a bowl and leave to stand for 15 minutes.
|
124
|
-
3. Melt the butter in a large non-stick frying pan.
|
125
|
-
OUTPUT
|
126
|
-
|
127
|
-
expect(formatter.to_s).to eq(expected)
|
128
|
-
end
|
18
|
+
it "formats ingredients with quantities and units" do
|
19
|
+
expect(output).to match(/shallots\s+2/)
|
20
|
+
expect(output).to match(/garlic\s+4 cloves/)
|
21
|
+
expect(output).to match(/msg\s+0\.5 tsp/)
|
22
|
+
expect(output).to match(/white pepper\s+some/)
|
129
23
|
end
|
130
24
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
it "returns empty string" do
|
136
|
-
expect(formatter.to_s).to eq("")
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
context "with various ingredient formats" do
|
141
|
-
let(:ingredients) do
|
142
|
-
[
|
143
|
-
Cooklang::Ingredient.new(name: "onion", quantity: 1),
|
144
|
-
Cooklang::Ingredient.new(name: "olive oil", unit: "drizzle"),
|
145
|
-
Cooklang::Ingredient.new(name: "salt"),
|
146
|
-
Cooklang::Ingredient.new(name: "pepper", quantity: "some", unit: "grinds")
|
147
|
-
]
|
148
|
-
end
|
149
|
-
let(:steps) { [] }
|
150
|
-
|
151
|
-
it "handles missing quantities and units gracefully" do
|
152
|
-
expected = <<~OUTPUT.strip
|
153
|
-
Ingredients:
|
154
|
-
onion 1
|
155
|
-
olive oil drizzle
|
156
|
-
salt some
|
157
|
-
pepper some grinds
|
158
|
-
OUTPUT
|
159
|
-
|
160
|
-
expect(formatter.to_s).to eq(expected)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
describe "#format_quantity_unit" do
|
166
|
-
let(:formatter) { described_class.new(recipe) }
|
167
|
-
let(:recipe) { double("recipe") }
|
25
|
+
it "formats steps with sequential numbering" do
|
26
|
+
lines = output.split("\n")
|
27
|
+
step_lines = lines.select { |line| line.match(/^\s*\d+\./) }
|
168
28
|
|
169
|
-
|
170
|
-
|
171
|
-
expect(
|
29
|
+
expect(step_lines.size).to be >= 10
|
30
|
+
expect(step_lines.first).to match(/1\..*/)
|
31
|
+
expect(step_lines.last).to match(/\d+\..*/)
|
172
32
|
end
|
173
33
|
|
174
|
-
it "
|
175
|
-
|
176
|
-
expect(
|
34
|
+
it "includes ingredient names in step text" do
|
35
|
+
expect(output).to include("shallots")
|
36
|
+
expect(output).to include("garlic")
|
37
|
+
expect(output).to include("day-old rice")
|
177
38
|
end
|
178
39
|
|
179
|
-
it "
|
180
|
-
|
181
|
-
expect(
|
40
|
+
it "includes timer references in steps" do
|
41
|
+
expect(output).to include("2 minutes")
|
42
|
+
expect(output).to include("30 seconds")
|
43
|
+
expect(output).to include("quick scramble")
|
182
44
|
end
|
183
45
|
|
184
|
-
it "
|
185
|
-
|
186
|
-
expect(
|
46
|
+
it "includes cookware references" do
|
47
|
+
expect(output).to include("wok")
|
48
|
+
expect(output).to include("mixing bowl")
|
187
49
|
end
|
188
50
|
end
|
189
51
|
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Canonical Metadata Support" do
|
6
|
+
let(:parser) { Cooklang::Parser.new }
|
7
|
+
|
8
|
+
describe "Complex YAML frontmatter" do
|
9
|
+
it "supports arrays in metadata" do
|
10
|
+
recipe_text = <<~COOK
|
11
|
+
---
|
12
|
+
title: Pasta Dish
|
13
|
+
tags: [italian, pasta, quick]
|
14
|
+
diet:
|
15
|
+
- vegetarian
|
16
|
+
- gluten-free
|
17
|
+
---
|
18
|
+
@pasta{200%g}
|
19
|
+
COOK
|
20
|
+
|
21
|
+
recipe = parser.parse(recipe_text)
|
22
|
+
|
23
|
+
expect(recipe.metadata["tags"]).to eq(["italian", "pasta", "quick"])
|
24
|
+
expect(recipe.metadata["diet"]).to eq(["vegetarian", "gluten-free"])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "supports nested hash structures" do
|
28
|
+
recipe_text = <<~COOK
|
29
|
+
---
|
30
|
+
title: Recipe
|
31
|
+
source:
|
32
|
+
name: My Cookbook
|
33
|
+
url: https://example.com/recipe
|
34
|
+
author: Jane Doe
|
35
|
+
time:
|
36
|
+
prep: 15 minutes
|
37
|
+
cook: 30 minutes
|
38
|
+
---
|
39
|
+
@flour{100%g}
|
40
|
+
COOK
|
41
|
+
|
42
|
+
recipe = parser.parse(recipe_text)
|
43
|
+
|
44
|
+
expect(recipe.metadata["source"]).to be_a(Hash)
|
45
|
+
expect(recipe.metadata["source"]["name"]).to eq("My Cookbook")
|
46
|
+
expect(recipe.metadata["source"]["url"]).to eq("https://example.com/recipe")
|
47
|
+
expect(recipe.metadata["source"]["author"]).to eq("Jane Doe")
|
48
|
+
|
49
|
+
expect(recipe.metadata["time"]).to be_a(Hash)
|
50
|
+
expect(recipe.metadata["time"]["prep"]).to eq("15 minutes")
|
51
|
+
expect(recipe.metadata["time"]["cook"]).to eq("30 minutes")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "supports mixed data types" do
|
55
|
+
recipe_text = <<~COOK
|
56
|
+
---
|
57
|
+
title: Test Recipe
|
58
|
+
servings: 4
|
59
|
+
rating: 4.5
|
60
|
+
published: true
|
61
|
+
custom_field: Some value
|
62
|
+
numbers: [1, 2, 3]
|
63
|
+
---
|
64
|
+
@salt
|
65
|
+
COOK
|
66
|
+
|
67
|
+
recipe = parser.parse(recipe_text)
|
68
|
+
|
69
|
+
expect(recipe.metadata["title"]).to eq("Test Recipe")
|
70
|
+
expect(recipe.metadata["servings"]).to eq(4)
|
71
|
+
expect(recipe.metadata["rating"]).to eq(4.5)
|
72
|
+
expect(recipe.metadata["published"]).to eq(true)
|
73
|
+
expect(recipe.metadata["custom_field"]).to eq("Some value")
|
74
|
+
expect(recipe.metadata["numbers"]).to eq([1, 2, 3])
|
75
|
+
end
|
76
|
+
|
77
|
+
it "raises error on invalid YAML" do
|
78
|
+
recipe_text = <<~COOK
|
79
|
+
---
|
80
|
+
title: Test
|
81
|
+
tags: [invalid
|
82
|
+
not closed
|
83
|
+
---
|
84
|
+
@salt
|
85
|
+
COOK
|
86
|
+
|
87
|
+
expect { parser.parse(recipe_text) }.to raise_error(Psych::SyntaxError)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "Canonical fields work as plain Hash values" do
|
92
|
+
let(:metadata) { Cooklang::Metadata.new }
|
93
|
+
|
94
|
+
it "stores servings as number or string" do
|
95
|
+
metadata["servings"] = 4
|
96
|
+
expect(metadata["servings"]).to eq(4)
|
97
|
+
|
98
|
+
metadata["servings"] = "6 cups worth"
|
99
|
+
expect(metadata["servings"]).to eq("6 cups worth")
|
100
|
+
end
|
101
|
+
|
102
|
+
it "stores time fields as nested or flat" do
|
103
|
+
metadata["time"] = { "prep" => "10 min", "cook" => "20 min" }
|
104
|
+
expect(metadata["time"]["prep"]).to eq("10 min")
|
105
|
+
expect(metadata["time"]["cook"]).to eq("20 min")
|
106
|
+
|
107
|
+
metadata.clear
|
108
|
+
metadata["prep_time"] = "15 minutes"
|
109
|
+
metadata["cook_time"] = "25 minutes"
|
110
|
+
expect(metadata["prep_time"]).to eq("15 minutes")
|
111
|
+
expect(metadata["cook_time"]).to eq("25 minutes")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "stores source as string or nested hash" do
|
115
|
+
metadata["source"] = {
|
116
|
+
"name" => "The Cookbook",
|
117
|
+
"url" => "https://example.com",
|
118
|
+
"author" => "Chef Name"
|
119
|
+
}
|
120
|
+
expect(metadata["source"]["name"]).to eq("The Cookbook")
|
121
|
+
expect(metadata["source"]["url"]).to eq("https://example.com")
|
122
|
+
expect(metadata["source"]["author"]).to eq("Chef Name")
|
123
|
+
|
124
|
+
metadata.clear
|
125
|
+
metadata["source"] = "Grandma's recipe box"
|
126
|
+
expect(metadata["source"]).to eq("Grandma's recipe box")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "stores tags and diet as arrays" do
|
130
|
+
metadata["tags"] = ["pasta", "italian"]
|
131
|
+
expect(metadata["tags"]).to eq(["pasta", "italian"])
|
132
|
+
|
133
|
+
metadata["diet"] = ["vegan", "gluten-free"]
|
134
|
+
expect(metadata["diet"]).to eq(["vegan", "gluten-free"])
|
135
|
+
end
|
136
|
+
|
137
|
+
it "stores other canonical fields" do
|
138
|
+
metadata["category"] = "dessert"
|
139
|
+
metadata["description"] = "A delicious recipe"
|
140
|
+
metadata["difficulty"] = "easy"
|
141
|
+
metadata["cuisine"] = "Italian"
|
142
|
+
metadata["locale"] = "en_US"
|
143
|
+
metadata["images"] = ["url1", "url2"]
|
144
|
+
|
145
|
+
expect(metadata["category"]).to eq("dessert")
|
146
|
+
expect(metadata["description"]).to eq("A delicious recipe")
|
147
|
+
expect(metadata["difficulty"]).to eq("easy")
|
148
|
+
expect(metadata["cuisine"]).to eq("Italian")
|
149
|
+
expect(metadata["locale"]).to eq("en_US")
|
150
|
+
expect(metadata["images"]).to eq(["url1", "url2"])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "Custom metadata fields" do
|
155
|
+
it "allows any custom fields" do
|
156
|
+
metadata = Cooklang::Metadata.new(
|
157
|
+
"wine_pairing" => "Pinot Noir",
|
158
|
+
"my_rating" => 5,
|
159
|
+
"special_equipment" => ["thermometer", "stand mixer"],
|
160
|
+
"notes_to_self" => "Double the garlic next time"
|
161
|
+
)
|
162
|
+
|
163
|
+
expect(metadata["wine_pairing"]).to eq("Pinot Noir")
|
164
|
+
expect(metadata["my_rating"]).to eq(5)
|
165
|
+
expect(metadata["special_equipment"]).to eq(["thermometer", "stand mixer"])
|
166
|
+
expect(metadata["notes_to_self"]).to eq("Double the garlic next time")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -81,161 +81,31 @@ RSpec.describe Cooklang::Metadata do
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
-
describe "
|
84
|
+
describe "direct access to metadata" do
|
85
85
|
let(:metadata) { described_class.new }
|
86
86
|
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
it "stores any data types" do
|
88
|
+
metadata["servings"] = 4
|
89
|
+
metadata["tags"] = ["pasta", "italian"]
|
90
|
+
metadata["rating"] = 4.5
|
91
|
+
metadata["published"] = true
|
92
|
+
metadata["source"] = { "name" => "Cookbook", "url" => "https://example.com" }
|
90
93
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
describe "#servings=" do
|
100
|
-
it "sets servings" do
|
101
|
-
metadata.servings = 6
|
102
|
-
|
103
|
-
expect(metadata["servings"]).to eq(6)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
describe "#prep_time" do
|
108
|
-
it "returns prep_time" do
|
109
|
-
metadata["prep_time"] = "15 minutes"
|
110
|
-
|
111
|
-
expect(metadata.prep_time).to eq("15 minutes")
|
112
|
-
end
|
113
|
-
|
114
|
-
it "returns prep-time as fallback" do
|
115
|
-
metadata["prep-time"] = "15 minutes"
|
116
|
-
|
117
|
-
expect(metadata.prep_time).to eq("15 minutes")
|
118
|
-
end
|
119
|
-
|
120
|
-
it "returns nil when not set" do
|
121
|
-
expect(metadata.prep_time).to be_nil
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
describe "#prep_time=" do
|
126
|
-
it "sets prep_time" do
|
127
|
-
metadata.prep_time = "20 minutes"
|
128
|
-
|
129
|
-
expect(metadata["prep_time"]).to eq("20 minutes")
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
describe "#cook_time" do
|
134
|
-
it "returns cook_time" do
|
135
|
-
metadata["cook_time"] = "30 minutes"
|
136
|
-
|
137
|
-
expect(metadata.cook_time).to eq("30 minutes")
|
138
|
-
end
|
139
|
-
|
140
|
-
it "returns cook-time as fallback" do
|
141
|
-
metadata["cook-time"] = "30 minutes"
|
142
|
-
|
143
|
-
expect(metadata.cook_time).to eq("30 minutes")
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
describe "#cook_time=" do
|
148
|
-
it "sets cook_time" do
|
149
|
-
metadata.cook_time = "25 minutes"
|
150
|
-
|
151
|
-
expect(metadata["cook_time"]).to eq("25 minutes")
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
describe "#total_time" do
|
156
|
-
it "returns total_time" do
|
157
|
-
metadata["total_time"] = "45 minutes"
|
158
|
-
|
159
|
-
expect(metadata.total_time).to eq("45 minutes")
|
160
|
-
end
|
161
|
-
|
162
|
-
it "returns total-time as fallback" do
|
163
|
-
metadata["total-time"] = "45 minutes"
|
164
|
-
|
165
|
-
expect(metadata.total_time).to eq("45 minutes")
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
describe "#total_time=" do
|
170
|
-
it "sets total_time" do
|
171
|
-
metadata.total_time = "50 minutes"
|
172
|
-
|
173
|
-
expect(metadata["total_time"]).to eq("50 minutes")
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
describe "#title" do
|
178
|
-
it "returns title" do
|
179
|
-
metadata["title"] = "Chocolate Cake"
|
180
|
-
|
181
|
-
expect(metadata.title).to eq("Chocolate Cake")
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
describe "#title=" do
|
186
|
-
it "sets title" do
|
187
|
-
metadata.title = "Vanilla Cake"
|
188
|
-
|
189
|
-
expect(metadata["title"]).to eq("Vanilla Cake")
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
describe "#source" do
|
194
|
-
it "returns source" do
|
195
|
-
metadata["source"] = "cookbook.com"
|
196
|
-
|
197
|
-
expect(metadata.source).to eq("cookbook.com")
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
describe "#source=" do
|
202
|
-
it "sets source" do
|
203
|
-
metadata.source = "my-blog.com"
|
204
|
-
|
205
|
-
expect(metadata["source"]).to eq("my-blog.com")
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
describe "#tags" do
|
210
|
-
it "returns array when tags is array" do
|
211
|
-
metadata["tags"] = ["dessert", "chocolate"]
|
212
|
-
|
213
|
-
expect(metadata.tags).to eq(["dessert", "chocolate"])
|
214
|
-
end
|
215
|
-
|
216
|
-
it "splits string tags on comma" do
|
217
|
-
metadata["tags"] = "dessert, chocolate, sweet"
|
218
|
-
|
219
|
-
expect(metadata.tags).to eq(["dessert", "chocolate", "sweet"])
|
220
|
-
end
|
221
|
-
|
222
|
-
it "returns empty array when not set" do
|
223
|
-
expect(metadata.tags).to eq([])
|
224
|
-
end
|
225
|
-
|
226
|
-
it "returns empty array for non-string, non-array values" do
|
227
|
-
metadata["tags"] = 123
|
228
|
-
|
229
|
-
expect(metadata.tags).to eq([])
|
230
|
-
end
|
94
|
+
expect(metadata["servings"]).to eq(4)
|
95
|
+
expect(metadata["tags"]).to eq(["pasta", "italian"])
|
96
|
+
expect(metadata["rating"]).to eq(4.5)
|
97
|
+
expect(metadata["published"]).to eq(true)
|
98
|
+
expect(metadata["source"]).to eq({ "name" => "Cookbook", "url" => "https://example.com" })
|
231
99
|
end
|
232
100
|
|
233
|
-
|
234
|
-
|
235
|
-
|
101
|
+
it "allows any custom fields" do
|
102
|
+
metadata["wine_pairing"] = "Pinot Noir"
|
103
|
+
metadata["my_custom_field"] = { "nested" => "data" }
|
104
|
+
metadata["notes"] = ["note1", "note2"]
|
236
105
|
|
237
|
-
|
238
|
-
|
106
|
+
expect(metadata["wine_pairing"]).to eq("Pinot Noir")
|
107
|
+
expect(metadata["my_custom_field"]).to eq({ "nested" => "data" })
|
108
|
+
expect(metadata["notes"]).to eq(["note1", "note2"])
|
239
109
|
end
|
240
110
|
end
|
241
111
|
end
|
data/spec/models/recipe_spec.rb
CHANGED
@@ -143,7 +143,7 @@ RSpec.describe Cooklang::Recipe do
|
|
143
143
|
|
144
144
|
let(:expected_hash) do
|
145
145
|
{
|
146
|
-
metadata: { map: { "title" => "Test Recipe", "servings" =>
|
146
|
+
metadata: { map: { "title" => "Test Recipe", "servings" => 4, "prep_time" => "15 minutes" } },
|
147
147
|
sections: [
|
148
148
|
{
|
149
149
|
name: nil,
|
@@ -174,7 +174,7 @@ RSpec.describe Cooklang::Recipe do
|
|
174
174
|
|
175
175
|
let(:expected_json) do
|
176
176
|
{
|
177
|
-
"metadata" => { "map" => { "title" => "Test Recipe", "servings" =>
|
177
|
+
"metadata" => { "map" => { "title" => "Test Recipe", "servings" => 4, "prep_time" => "15 minutes" } },
|
178
178
|
"sections" => [
|
179
179
|
{
|
180
180
|
"name" => nil,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cooklang
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Brooks
|
@@ -13,16 +13,16 @@ dependencies:
|
|
13
13
|
name: bundler
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '2.
|
18
|
+
version: '2.1'
|
19
19
|
type: :development
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
|
-
- - "
|
23
|
+
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '2.
|
25
|
+
version: '2.1'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rake
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,7 +124,6 @@ files:
|
|
124
124
|
- ".rspec"
|
125
125
|
- ".rubocop.yml"
|
126
126
|
- Gemfile
|
127
|
-
- Gemfile.lock
|
128
127
|
- LICENSE.txt
|
129
128
|
- README.md
|
130
129
|
- Rakefile
|
@@ -158,8 +157,13 @@ files:
|
|
158
157
|
- spec/comprehensive_spec.rb
|
159
158
|
- spec/cooklang_spec.rb
|
160
159
|
- spec/fixtures/canonical.yaml
|
160
|
+
- spec/fixtures/comprehensive_recipe.cook
|
161
|
+
- spec/formatters/formatter_spec.rb
|
162
|
+
- spec/formatters/hash_spec.rb
|
163
|
+
- spec/formatters/json_spec.rb
|
161
164
|
- spec/formatters/text_spec.rb
|
162
165
|
- spec/integration/canonical_spec.rb
|
166
|
+
- spec/integration/metadata_canonical_spec.rb
|
163
167
|
- spec/lexer_spec.rb
|
164
168
|
- spec/models/cookware_spec.rb
|
165
169
|
- spec/models/ingredient_spec.rb
|
@@ -186,7 +190,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
186
190
|
requirements:
|
187
191
|
- - ">="
|
188
192
|
- !ruby/object:Gem::Version
|
189
|
-
version:
|
193
|
+
version: 2.7.0
|
190
194
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
195
|
requirements:
|
192
196
|
- - ">="
|
@@ -200,8 +204,13 @@ test_files:
|
|
200
204
|
- spec/comprehensive_spec.rb
|
201
205
|
- spec/cooklang_spec.rb
|
202
206
|
- spec/fixtures/canonical.yaml
|
207
|
+
- spec/fixtures/comprehensive_recipe.cook
|
208
|
+
- spec/formatters/formatter_spec.rb
|
209
|
+
- spec/formatters/hash_spec.rb
|
210
|
+
- spec/formatters/json_spec.rb
|
203
211
|
- spec/formatters/text_spec.rb
|
204
212
|
- spec/integration/canonical_spec.rb
|
213
|
+
- spec/integration/metadata_canonical_spec.rb
|
205
214
|
- spec/lexer_spec.rb
|
206
215
|
- spec/models/cookware_spec.rb
|
207
216
|
- spec/models/ingredient_spec.rb
|