brewser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +13 -0
- data/lib/brewser.rb +32 -0
- data/lib/brewser/brewson.rb +5 -0
- data/lib/brewser/engines.rb +18 -0
- data/lib/brewser/engines/beerxml.rb +433 -0
- data/lib/brewser/engines/beerxml2.rb +28 -0
- data/lib/brewser/engines/brewson.rb +20 -0
- data/lib/brewser/engines/promash_rec.rb +424 -0
- data/lib/brewser/engines/promash_txt.rb +214 -0
- data/lib/brewser/exceptions.rb +11 -0
- data/lib/brewser/json-validation.rb +12 -0
- data/lib/brewser/model/additive.rb +14 -0
- data/lib/brewser/model/base.rb +45 -0
- data/lib/brewser/model/batch.rb +27 -0
- data/lib/brewser/model/fermentable.rb +28 -0
- data/lib/brewser/model/fermentation_schedule.rb +7 -0
- data/lib/brewser/model/fermentation_steps.rb +12 -0
- data/lib/brewser/model/hop.rb +27 -0
- data/lib/brewser/model/mash_schedule.rb +12 -0
- data/lib/brewser/model/mash_steps.rb +24 -0
- data/lib/brewser/model/recipe.rb +50 -0
- data/lib/brewser/model/style.rb +29 -0
- data/lib/brewser/model/units.rb +102 -0
- data/lib/brewser/model/water_profile.rb +16 -0
- data/lib/brewser/model/yeast.rb +29 -0
- data/lib/brewser/ruby-units.rb +31 -0
- data/lib/brewser/version.rb +3 -0
- data/spec/basic_spec.rb +39 -0
- data/spec/beerxml_spec.rb +159 -0
- data/spec/promash_spec.rb +194 -0
- data/spec/spec_helper.rb +29 -0
- metadata +187 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
class BeerXML2 < Brewser::Engine
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def acceptable?(q)
|
8
|
+
Nokogiri::XML(q){|config| config.noblanks }.remove_namespaces!.xpath("//version").inner_text.to_i == 2 ? true : false
|
9
|
+
# @TODO Add XSD Validation here
|
10
|
+
end
|
11
|
+
|
12
|
+
def deserialize(string_or_io)
|
13
|
+
results = parse_xml(string_or_io)
|
14
|
+
puts results.inspect
|
15
|
+
return results
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize(brewser_model)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_xml(string_or_io)
|
22
|
+
raise NotImplemented, "BeerXML2 support is not implemented yet"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,424 @@
|
|
1
|
+
class ProMashRec < Brewser::Engine
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def acceptable?(q)
|
6
|
+
return true
|
7
|
+
end
|
8
|
+
|
9
|
+
def deserialize(io)
|
10
|
+
object = ProMashRec::Recipe.new
|
11
|
+
object.from_promash(io)
|
12
|
+
return object
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module ::Loader
|
18
|
+
|
19
|
+
require 'bindata'
|
20
|
+
|
21
|
+
class RecEntry < BinData::Record
|
22
|
+
endian :little
|
23
|
+
end
|
24
|
+
|
25
|
+
class Hop < RecEntry
|
26
|
+
string :name, :length => 55, :trim_padding => true
|
27
|
+
float :alpha_acids
|
28
|
+
float :beta_acids
|
29
|
+
int8 :noble
|
30
|
+
float :cohumulone
|
31
|
+
float :myrcene
|
32
|
+
float :humulene
|
33
|
+
float :caryphylene
|
34
|
+
int8 :type
|
35
|
+
int8 :form
|
36
|
+
float :storage_factor
|
37
|
+
string :taste_notes, :length => 155, :trim_padding => true
|
38
|
+
string :origin, :length => 55, :trim_padding => true
|
39
|
+
string :best_for, :length => 155, :trim_padding => true
|
40
|
+
string :substitutes, :length => 155, :trim_padding => true
|
41
|
+
skip length: 10
|
42
|
+
float :storage_temp_c
|
43
|
+
float :actual_alpha_acids
|
44
|
+
int8 :flag
|
45
|
+
float :amount
|
46
|
+
int16 :time
|
47
|
+
float :ibus
|
48
|
+
end
|
49
|
+
|
50
|
+
class Fermentable < RecEntry
|
51
|
+
string :name, :length => 110, :trim_padding => true
|
52
|
+
string :origin, :length => 55, :trim_padding => true
|
53
|
+
int8 :type
|
54
|
+
int8 :form
|
55
|
+
float :potential
|
56
|
+
float :color
|
57
|
+
float :moisture_content
|
58
|
+
float :max_in_batch
|
59
|
+
float :diastatic_power
|
60
|
+
float :percent_protein
|
61
|
+
float :percent_tsn
|
62
|
+
string :uses, :length => 155, :trim_padding => true
|
63
|
+
string :comments, :length => 155, :trim_padding => true
|
64
|
+
skip length: 8
|
65
|
+
float :dry_basis_fg
|
66
|
+
float :dry_basis_cg
|
67
|
+
float :amount
|
68
|
+
float :extract
|
69
|
+
end
|
70
|
+
|
71
|
+
class Additive < RecEntry
|
72
|
+
string :name, :length => 55, :trim_padding => true
|
73
|
+
int8 :type
|
74
|
+
int16 :time
|
75
|
+
skip length: 2
|
76
|
+
int8 :added_when
|
77
|
+
int8 :time_in
|
78
|
+
int8 :amount_unit
|
79
|
+
skip length: 4
|
80
|
+
string :uses, :length => 255, :trim_padding => true
|
81
|
+
string :comments, :length => 255, :trim_padding => true
|
82
|
+
float :amount
|
83
|
+
skip length: 8
|
84
|
+
end
|
85
|
+
|
86
|
+
class Yeast < RecEntry
|
87
|
+
string :name, :length => 55, :trim_padding => true
|
88
|
+
string :supplier, :length => 55, :trim_padding => true
|
89
|
+
string :catalog, :length => 26, :trim_padding => true
|
90
|
+
string :flavor_note, :length => 155, :trim_padding => true
|
91
|
+
string :comment, :length => 155, :trim_padding => true
|
92
|
+
skip length: 9
|
93
|
+
int32 :aa_low
|
94
|
+
int32 :aa_high
|
95
|
+
float :optimum_temp
|
96
|
+
skip length: 6
|
97
|
+
end
|
98
|
+
|
99
|
+
class WaterProfile < RecEntry
|
100
|
+
string :name, :length => 27, :trim_padding => true
|
101
|
+
float :calcium
|
102
|
+
float :magnesium
|
103
|
+
float :sodium
|
104
|
+
float :unknown
|
105
|
+
float :sulfate
|
106
|
+
float :chloride
|
107
|
+
float :bicarbonate
|
108
|
+
float :ph
|
109
|
+
string :comments, :length => 155, :trim_padding => true
|
110
|
+
skip length: 8
|
111
|
+
end
|
112
|
+
|
113
|
+
class MashStep < RecEntry
|
114
|
+
string :name, :length => 255, :trim_padding => true
|
115
|
+
int8 :type
|
116
|
+
int32 :start_temp
|
117
|
+
int32 :stop_temp
|
118
|
+
int32 :step_temp
|
119
|
+
int32 :rest_time
|
120
|
+
int32 :step_time
|
121
|
+
float :thickness
|
122
|
+
float :amount
|
123
|
+
skip length: 8
|
124
|
+
end
|
125
|
+
|
126
|
+
class MashSchedule < RecEntry
|
127
|
+
int32 :_steps_count
|
128
|
+
int32 :grain_temp
|
129
|
+
skip length: 4
|
130
|
+
array :mash_steps, :type => :mash_step, :read_until => lambda { index == 50 }
|
131
|
+
string :name, :length => 256, :trim_padding => true
|
132
|
+
end
|
133
|
+
|
134
|
+
class SimpleSchedule < RecEntry
|
135
|
+
int32 :num_steps
|
136
|
+
skip length: 5
|
137
|
+
int32 :acid_rest_temp
|
138
|
+
int32 :acid_rest_time
|
139
|
+
int32 :protein_rest_temp
|
140
|
+
int32 :protein_rest_time
|
141
|
+
int32 :int_rest_temp
|
142
|
+
int32 :int_rest_time
|
143
|
+
int32 :sac_rest_temp
|
144
|
+
int32 :sac_rest_time
|
145
|
+
int32 :mash_out_temp
|
146
|
+
int32 :mash_out_time
|
147
|
+
int32 :sparge_temp
|
148
|
+
int32 :sparge_time
|
149
|
+
float :mash_volume
|
150
|
+
skip length: 1
|
151
|
+
end
|
152
|
+
|
153
|
+
class Style < RecEntry
|
154
|
+
string :category, :length => 55, :trim_padding => true
|
155
|
+
string :sub_category, :length => 55, :trim_padding => true
|
156
|
+
int8 :_type
|
157
|
+
float :min_og
|
158
|
+
float :max_og
|
159
|
+
float :min_fg
|
160
|
+
float :max_fg
|
161
|
+
float :min_abv
|
162
|
+
float :max_abv
|
163
|
+
float :min_ibu
|
164
|
+
float :max_ibu
|
165
|
+
float :min_color
|
166
|
+
float :max_color
|
167
|
+
string :color_note, :length => 155, :trim_padding => true
|
168
|
+
string :malt_note, :length => 155, :trim_padding => true
|
169
|
+
string :hop_note, :length => 155, :trim_padding => true
|
170
|
+
string :yeast_note, :length => 155, :trim_padding => true
|
171
|
+
string :examples, :length => 255, :trim_padding => true
|
172
|
+
int16 :category_number
|
173
|
+
string :sub_category_letter, :length => 2, :trim_padding => true
|
174
|
+
int8 :_guideline
|
175
|
+
end
|
176
|
+
|
177
|
+
class Recipe < RecEntry
|
178
|
+
string :title, :length => 85, :trim_padding => true
|
179
|
+
int32 :_hops_count
|
180
|
+
int32 :_fermentables_count
|
181
|
+
int32 :_additives_count
|
182
|
+
float :recipe_volume
|
183
|
+
float :boil_volume
|
184
|
+
float :estimated_og
|
185
|
+
float :estimated_ibu
|
186
|
+
float :recipe_efficiency
|
187
|
+
int32 :boil_time
|
188
|
+
int32 :_recipe_type
|
189
|
+
int8 :_brew_method
|
190
|
+
style :style
|
191
|
+
array :hops, :type => :hop, :initial_length => :_hops_count
|
192
|
+
array :fermentables, :type => :fermentable, :initial_length => :_fermentables_count
|
193
|
+
array :additives, :type => :additive, :initial_length => :_additives_count
|
194
|
+
yeast :yeast
|
195
|
+
water_profile :water_profile
|
196
|
+
simple_schedule :simple_schedule
|
197
|
+
string :notes, :length => 4029, :trim_padding => true
|
198
|
+
string :awards, :length => 3693, :trim_padding => true
|
199
|
+
mash_schedule :mash_schedule
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
class ProMashRec::Hop < Brewser::Hop
|
207
|
+
|
208
|
+
@@hop_types = {0 => "Both", 1 => "Bittering", 2 => "Aroma"}
|
209
|
+
@@hop_forms = {1 => "Whole", 3 => "Whole", 21 => "Pellet", 23 => "Pellet", 11 => "Plug"}
|
210
|
+
|
211
|
+
# Pellet 21, 23
|
212
|
+
def from_promash(hop,time)
|
213
|
+
self.name = hop.name.split("\x00")[0]
|
214
|
+
self.origin = hop.origin.split("\x00")[0]
|
215
|
+
self.alpha_acids = hop.alpha_acids
|
216
|
+
self.beta_acids = hop.beta_acids
|
217
|
+
self.type = @@hop_types[hop.type]
|
218
|
+
self.form = @@hop_forms[hop.form]
|
219
|
+
self.amount = "#{hop.amount} oz".u
|
220
|
+
self.time = "#{hop.time} min".u
|
221
|
+
self.storageability = hop.storage_factor
|
222
|
+
self.humulene = hop.humulene
|
223
|
+
self.caryophyllene = hop.caryphylene
|
224
|
+
self.cohumulone = hop.cohumulone
|
225
|
+
self.myrcene = hop.myrcene
|
226
|
+
if hop.time.to_i >= 0 and hop.time.to_i <= time
|
227
|
+
self.added_when = "Boil"
|
228
|
+
elsif hop.time.to_i == time+1
|
229
|
+
self.added_when = "FWH"
|
230
|
+
elsif hop.time.to_i == time+2
|
231
|
+
self.added_when = "Mash"
|
232
|
+
else
|
233
|
+
self.added_when = "Dry"
|
234
|
+
end
|
235
|
+
|
236
|
+
return self
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
class ProMashRec::Fermentable < Brewser::Fermentable
|
242
|
+
|
243
|
+
@@ferm_types = {0 => { 0 => "Grain", 1 => "Dry Extract", 2 => "Sugar", 3 => "Adjunct"},
|
244
|
+
1 => { 0 => "Grain", 1 => "Extract", 2 => "Sugar", 3 => "Adjunct"}}
|
245
|
+
|
246
|
+
def from_promash(ferm)
|
247
|
+
self.name = ferm.name.split("\x00")[0]
|
248
|
+
self.origin = ferm.origin.split("\x00")[0]
|
249
|
+
self.type = @@ferm_types[ferm.form][ferm.type]
|
250
|
+
self.potential = ferm.potential.round(3)
|
251
|
+
self.color = ferm.color
|
252
|
+
self.amount = "#{ferm.amount} lbs".u
|
253
|
+
self.diastatic_power = ferm.diastatic_power
|
254
|
+
self.max_in_batch = ferm.max_in_batch
|
255
|
+
self.moisture = ferm.moisture_content
|
256
|
+
|
257
|
+
return self
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
class ProMashRec::Additive < Brewser::Additive
|
263
|
+
|
264
|
+
@@additive_types = { 0 => "Spice", 1 => "Fruit", 2 => "Flavor", 3 => "Other", 4 => "Fining", 5 => "Herb" }
|
265
|
+
@@added_whens = { 0 => "Boil", 1 => "Primary", 2 => "Mash" }
|
266
|
+
@@additive_unit = { 0 => "oz", 1 => "g", 2 => "lb", 3 => "tsp", 4 => "tbsp", 5 => "cups", 6 => "each" }
|
267
|
+
@@additive_time = { 0 => "days", 1 => "min" }
|
268
|
+
|
269
|
+
def from_promash(add)
|
270
|
+
self.name = add.name.split("\x00")[0]
|
271
|
+
self.type = @@additive_types[add.type]
|
272
|
+
self.added_when = @@added_whens[add.added_when]
|
273
|
+
self.time = "#{add.time} #{@@additive_time[add.time_in]}".u
|
274
|
+
self.amount = "#{add.amount} #{@@additive_unit[add.amount_unit]}".u
|
275
|
+
self.use_for = add.uses
|
276
|
+
|
277
|
+
return self
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
class ProMashRec::Yeast < Brewser::Yeast
|
283
|
+
|
284
|
+
def from_promash(y)
|
285
|
+
self.name = y.name.split("\x00")[0]
|
286
|
+
self.supplier = y.supplier
|
287
|
+
self.catalog = y.catalog
|
288
|
+
self.attenuation = (y.aa_high + y.aa_low)/2
|
289
|
+
self.min_temperature = self.max_temperature = "#{y.optimum_temp} dF".u
|
290
|
+
self.description = y.comment
|
291
|
+
self.amount = "1 each".u
|
292
|
+
|
293
|
+
return self
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
class ProMashRec::MashStep < Brewser::MashStep
|
299
|
+
|
300
|
+
def from_simple(idx, name, rest_temp, rest_time)
|
301
|
+
self.index = idx
|
302
|
+
self.name = name
|
303
|
+
self.rest_temperature = rest_temp.u
|
304
|
+
self.rest_time = rest_time.u
|
305
|
+
|
306
|
+
return self
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
class ProMashRec::MashSchedule < Brewser::MashSchedule
|
312
|
+
|
313
|
+
def from_promash(mash)
|
314
|
+
|
315
|
+
return self
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
class ProMashRec::SimpleSchedule < Brewser::MashSchedule
|
321
|
+
|
322
|
+
def from_promash(mash)
|
323
|
+
self.name = "Simple"
|
324
|
+
["acid_rest", "protein_rest", "int_rest", "sac_rest", "mash_out", "sparge"].each do |step, index|
|
325
|
+
next if mash.send("#{step}_temp")==0
|
326
|
+
self.mash_steps.push ProMashRec::MashStep.new.from_simple(index, "#{step.gsub("_"," ").capitalize}", "#{mash.send("#{step}_temp")} dF",
|
327
|
+
"#{mash.send("#{step}_time")} min")
|
328
|
+
end
|
329
|
+
return self
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
class ProMashRec::FermentationStep < Brewser::FermentationStep
|
335
|
+
# Not support in ProMash REC files
|
336
|
+
end
|
337
|
+
|
338
|
+
class ProMashRec::FermentationSchedule < Brewser::FermentationSchedule
|
339
|
+
# Not supported in ProMash REC files
|
340
|
+
end
|
341
|
+
|
342
|
+
class ProMashRec::WaterProfile < Brewser::WaterProfile
|
343
|
+
|
344
|
+
def from_promash(water)
|
345
|
+
self.name = water.name.split("\x00")[0]
|
346
|
+
self.calcium = water.calcium
|
347
|
+
self.magnesium = water.magnesium
|
348
|
+
self.sodium = water.sodium
|
349
|
+
self.chloride = water.chloride
|
350
|
+
self.sulfates = water.sulfate
|
351
|
+
self.bicarbonate = water.bicarbonate
|
352
|
+
self.ph = water.ph.round(2)
|
353
|
+
|
354
|
+
return self
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
class ProMashRec::Style < Brewser::Style
|
360
|
+
|
361
|
+
@@style_guides = { 0 => "AHA", 1 => "BJCP" }
|
362
|
+
@@style_types = { 0 => "Ale", 1 => "Lager", 2 => "Cider", 3 => "Mead", 5 => "Hybrid"}
|
363
|
+
|
364
|
+
def from_promash(style)
|
365
|
+
self.name = style.sub_category.split("\x00")[0]
|
366
|
+
self.category = style.category.split("\x00")[0]
|
367
|
+
self.category_number = style.category_number
|
368
|
+
self.style_letter = style.sub_category_letter
|
369
|
+
self.style_guide = @@style_guides[style._guideline]
|
370
|
+
self.type = @@style_types[style._type]
|
371
|
+
self.og_min = style.min_og
|
372
|
+
self.og_max = style.max_og
|
373
|
+
self.fg_min = style.min_fg
|
374
|
+
self.fg_max = style.max_fg
|
375
|
+
self.ibu_min = style.min_ibu
|
376
|
+
self.ibu_max = style.max_ibu
|
377
|
+
self.color_min = style.min_color
|
378
|
+
self.color_max = style.max_color
|
379
|
+
self.examples = style.examples
|
380
|
+
|
381
|
+
return self
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
|
386
|
+
class ProMashRec::Recipe < Brewser::Recipe
|
387
|
+
|
388
|
+
@@recipe_types = { 0 => "Other", 15 => "Lager", 25 => "Hybrid", 30 => "Ale" }
|
389
|
+
@@brew_methods = { 0 => "All Grain", 1 => "Partial Mash", 2 => "Extract" }
|
390
|
+
|
391
|
+
def from_promash(data)
|
392
|
+
rec = Loader::Recipe.read(data)
|
393
|
+
self.name = rec.title.split("\x00")[0]
|
394
|
+
self.method = @@brew_methods[rec._brew_method]
|
395
|
+
self.recipe_volume = "#{rec.recipe_volume} gal".u
|
396
|
+
self.boil_volume = "#{rec.boil_volume} gal".u
|
397
|
+
self.boil_time = "#{rec.boil_time} min".u
|
398
|
+
self.estimated_og = (1+rec.estimated_og/1000).round(3)
|
399
|
+
self.estimated_ibu = rec.estimated_ibu.round(1)
|
400
|
+
self.recipe_efficiency = rec.recipe_efficiency*100
|
401
|
+
self.style = ProMashRec::Style.new.from_promash(rec.style)
|
402
|
+
rec.hops.each do |hop|
|
403
|
+
self.hops.push ProMashRec::Hop.new.from_promash(hop,rec.boil_time.to_i)
|
404
|
+
end
|
405
|
+
rec.fermentables.each do |fermentable|
|
406
|
+
self.fermentables.push ProMashRec::Fermentable.new.from_promash(fermentable)
|
407
|
+
end
|
408
|
+
rec.additives.each do |additive|
|
409
|
+
self.additives.push ProMashRec::Additive.new.from_promash(additive)
|
410
|
+
end
|
411
|
+
self.yeasts.push ProMashRec::Yeast.new.from_promash(rec.yeast)
|
412
|
+
self.water_profile = ProMashRec::WaterProfile.new.from_promash(rec.water_profile)
|
413
|
+
if rec.simple_schedule.num_steps > 0
|
414
|
+
self.mash_schedule = ProMashRec::SimpleSchedule.new.from_promash(rec.simple_schedule)
|
415
|
+
else
|
416
|
+
self.mash_schedule = ProMashRec::MashSchedule.new.from_promash(rec.mash_schedule)
|
417
|
+
end
|
418
|
+
self.description = rec.notes
|
419
|
+
self.type = self.style.type
|
420
|
+
#puts rec.snapshot
|
421
|
+
return self
|
422
|
+
end
|
423
|
+
|
424
|
+
end
|