brewser 0.1.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
+ Copyright (c) 2012 Jon Lochner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
@@ -0,0 +1,13 @@
1
+ brewser
2
+ =======
3
+
4
+ Brewser is a ruby library for parsing and generating serialized brewing data
5
+
6
+ Currently brewser is very early in development but will eventually support the following input formats:
7
+ * [BeerXML (v1, v2)](http://beerxml.org)
8
+ * BrewSON - Brewser Recipe and Batches in JSON
9
+ * [ProMash (.rec and text exports)](http://www.promash.com)
10
+
11
+ Input files are deserialized into a common object model for consumption. Brewser supports these output formats:
12
+ * BeerXML
13
+ * BrewSON
@@ -0,0 +1,32 @@
1
+ require 'brewser/ruby-units'
2
+ require 'brewser/json-validation'
3
+ require 'brewser/model/base'
4
+ require 'brewser/exceptions'
5
+ require 'brewser/engines'
6
+
7
+ module Brewser
8
+
9
+ class << self
10
+
11
+ # Returns the potential engine to process the given string
12
+ def identify(string_or_io)
13
+ return BrewSON if BrewSON.acceptable?(string_or_io)
14
+ return BeerXML2 if BeerXML2.acceptable?(string_or_io)
15
+ return BeerXML if BeerXML.acceptable?(string_or_io)
16
+ return ProMashTxt if ProMashTxt.acceptable?(string_or_io)
17
+ return ProMashRec if ProMashRec.acceptable?(string_or_io)
18
+ raise Error, "unable to identify content"
19
+ end
20
+
21
+ # Executes the engine matching the given string
22
+ def parse(string_or_io)
23
+ if engine=self.identify(string_or_io)
24
+ engine.send(:deserialize, string_or_io)
25
+ else
26
+ return nil
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,5 @@
1
+ module Brewser
2
+ module BrewSON
3
+
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ module Brewser
2
+
3
+ # Skeleton for an engine
4
+ class Engine
5
+
6
+ def self.acceptable?(q)
7
+ return false
8
+ end
9
+
10
+ def self.deserialize(string_or_io)
11
+ return nil
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+
18
+ Dir.glob(File.dirname(__FILE__) + '/engines/*', &method(:require))
@@ -0,0 +1,433 @@
1
+ class BeerXML < Brewser::Engine
2
+
3
+ require 'nokogiri'
4
+
5
+ class << self
6
+
7
+ # Simple test to see if this looks like XML
8
+ def acceptable?(q)
9
+ Nokogiri::XML(q){|config| config.noblanks }.root ? true : false
10
+ end
11
+
12
+ # Attempt to deserialize the data
13
+ def deserialize(string_or_io)
14
+ begin
15
+ outer = Nokogiri::XML(string_or_io).root
16
+ # We expect to find a plural of one of the models
17
+ objects = (outer>outer.node_name.singularize).map do |inner|
18
+ ("BeerXML::#{inner.node_name.downcase.camelcase}".constantize).from_xml(inner)
19
+ end
20
+ return cleanup(objects)
21
+ # rescue
22
+ # raise Error, "BeerXML engine encountered an issue and can not continue"
23
+ end
24
+ end
25
+
26
+ # Ugly hack to deal with BeerXML oddities
27
+ def cleanup(brewser_objects)
28
+ brewser_objects.each(&:cleanup)
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+ require 'roxml'
35
+
36
+ #
37
+ #
38
+ # These models add the hooks to deserialize the data using ROXML
39
+ # Brought in as a seperate model to allow multiple version of BeerXML
40
+ class BeerXML::Hop < Brewser::Hop
41
+ include ROXML
42
+
43
+ xml_name "HOP"
44
+ xml_convention :upcase
45
+ xml_reader :name
46
+ xml_reader :origin
47
+ xml_reader :description, :from => "NOTES"
48
+
49
+ xml_reader :type
50
+ xml_reader(:form) { |x|
51
+ x="Whole" if x=="Leaf"
52
+ x
53
+ }
54
+
55
+ xml_reader :display_amount
56
+ xml_reader :uncast_amount, :from => "AMOUNT"
57
+
58
+ xml_reader :display_time
59
+ xml_reader :uncast_time, :from => "TIME"
60
+
61
+ xml_reader(:added_when, :from => "USE") { |x|
62
+ x="Dry" if x=="Dry Hop"
63
+ x="FWH" if x=="First Wort"
64
+ x="Boil" if x =="Aroma"
65
+ x
66
+ }
67
+
68
+ xml_reader :alpha_acids, :from => "ALPHA", :as => Float
69
+ xml_reader :beta_acids, :from => "BETA", :as => Float
70
+
71
+ xml_reader :humulene, :as => Float
72
+ xml_reader :caryophyllene, :as => Float
73
+ xml_reader :cohumulone, :as => Float
74
+ xml_reader :myrcene, :as => Float
75
+ xml_reader :farnesene, :as => Float # Not explicitly included in BeerXML
76
+ xml_reader :total_oil, :as => Float # Not explicitly included in BeerXML
77
+ xml_reader :storageability, :from => "HSI", :as => Float
78
+
79
+ xml_reader :substitutes
80
+
81
+ def cleanup
82
+ self.amount = display_amount.present? ? display_amount.u : "#{uncast_amount} kg".u
83
+ self.time = display_time.present? ? display_time.u : "#{uncast_time} min".u
84
+ end
85
+
86
+ end
87
+
88
+ class BeerXML::Fermentable < Brewser::Fermentable
89
+ include ROXML
90
+
91
+ xml_name "FERMENTABLE"
92
+ xml_convention :upcase
93
+ xml_reader :name
94
+ xml_reader :origin
95
+ xml_reader :supplier
96
+ xml_reader :description, :from => "NOTES"
97
+ xml_reader :type
98
+
99
+ xml_reader :display_amount
100
+ xml_reader :uncast_amount, :from => "AMOUNT"
101
+
102
+ xml_reader :yield_percent, :from => "YIELD"
103
+ xml_reader :uncast_potential, :from => "POTENTIAL"
104
+
105
+ xml_reader :color
106
+
107
+ xml_reader :late_addition?, :from => "ADD_AFTER_BOIL"
108
+ xml_reader :recommend_mash?, :from => "RECOMMEND_MASH"
109
+
110
+ xml_reader :max_in_batch, :as => Float
111
+ xml_reader(:diastatic_power) {|x| x.to_f }
112
+ xml_reader(:moisture) {|x| x.to_f }
113
+ xml_reader(:coarse_fine_diff) {|x| x.to_f }
114
+ xml_reader(:protein) {|x| x.to_f }
115
+ xml_reader(:ibu_gal_per_lb) {|x| x.to_f }
116
+
117
+ def cleanup
118
+ self.amount = display_amount.present? ? display_amount.u : "#{uncast_amount} kg".u
119
+ self.potential = uncast_potential.present? ? uncast_potential.to_f : 1+(46*(yield_percent/100))/1000
120
+ self.ppg = (potential-1)*1000
121
+ end
122
+
123
+ end
124
+
125
+ class BeerXML::Additive < Brewser::Additive
126
+ include ROXML
127
+
128
+ xml_name "MISC"
129
+ xml_convention :upcase
130
+ xml_reader :name
131
+ xml_reader :origin
132
+
133
+ xml_reader :type
134
+ xml_reader :form
135
+
136
+ xml_reader :display_amount
137
+ xml_reader :amount_scalar, :from => "AMOUNT"
138
+
139
+ xml_reader :weight?, :from => "AMOUNT_IS_WEIGHT"
140
+
141
+ xml_reader(:added_when, :from => "USE") { |x|
142
+ x="Packaging" if x == "Bottling"
143
+ x
144
+ }
145
+ xml_reader :use_for
146
+
147
+ xml_reader :display_time
148
+ xml_reader :uncast_time, :from => "TIME"
149
+
150
+ xml_reader :description, :from => "NOTES"
151
+
152
+ def set_amount
153
+ return display_amount.u unless display_amount.blank?
154
+ units = weight? ? "kg" : "l"
155
+ return "#{amount_scalar} #{units}".u
156
+ end
157
+
158
+ def cleanup
159
+ self.amount = set_amount
160
+ self.time = display_time.present? ? display_time.u : "#{time} min".u
161
+ end
162
+
163
+ end
164
+
165
+ class BeerXML::Yeast < Brewser::Yeast
166
+ include ROXML
167
+
168
+ xml_name "YEAST"
169
+ xml_convention :upcase
170
+ xml_reader :name
171
+ xml_reader :supplier, :from => "LABORATORY"
172
+ xml_reader :catalog, :from => "PRODUCT_ID"
173
+
174
+ xml_reader :type
175
+ xml_reader :form
176
+
177
+ xml_reader :display_amount
178
+ xml_reader :amount_scalar, :from => "AMOUNT"
179
+ xml_reader :weight?, :from => "AMOUNT_IS_WEIGHT"
180
+
181
+ xml_reader :uncast_min_temperature, :from => "MIN_TEMPERATURE"
182
+ xml_reader :disp_min_temp
183
+
184
+ xml_reader :uncast_max_temperature, :from => "MAX_TEMPERATURE"
185
+ xml_reader :disp_max_temp
186
+
187
+ xml_reader :flocculation
188
+ xml_reader :attenuation, :as => Float
189
+ xml_reader :best_for
190
+ xml_reader :add_to_secondary?, :from => "ADD_TO_SECONDARY"
191
+
192
+ xml_reader :times_cultured
193
+ xml_reader :max_reuse
194
+
195
+ xml_reader :description, :from => "NOTES"
196
+
197
+ def set_amount
198
+ return display_amount.u unless display_amount.blank?
199
+ units = weight? ? "kg" : "l"
200
+ return "#{amount_scalar} #{units}".u
201
+ end
202
+
203
+ def cleanup
204
+ self.amount = set_amount
205
+ self.min_temperature = disp_min_temp.present? ? disp_min_temp.u : "#{uncast_min_temperature} dC".u
206
+ self.max_temperature = disp_max_temp.present? ? disp_max_temp.u : "#{uncast_max_temperature} dC".u
207
+ end
208
+
209
+ end
210
+
211
+ class BeerXML::MashStep < Brewser::MashStep
212
+ include ROXML
213
+
214
+ xml_name "MASH_STEP"
215
+ xml_convention :upcase
216
+ xml_reader :name
217
+ xml_reader :description
218
+
219
+ xml_reader :purpose
220
+ xml_reader(:type) { |x|
221
+ x="Direct" if x=="Temperature"
222
+ x
223
+ }
224
+
225
+ xml_reader(:ramp_time) { |x| "#{x} min".u }
226
+ xml_reader(:rest_time, :from => "STEP_TIME") { |x| "#{x} min".u }
227
+
228
+ xml_reader :uncast_rest_temperature, :from => "STEP_TEMP"
229
+
230
+ xml_reader :water_to_grain_ratio, :from => "WATER_GRAIN_RATIO", :as => Float
231
+
232
+ xml_reader :uncast_infusion_volume, :from => "INFUSE_AMOUNT"
233
+ xml_reader :display_infuse_amt
234
+
235
+ xml_reader :infusion_temperature, :from => "INFUSE_TEMP"
236
+
237
+ property :step_volume, Volume
238
+ property :ramp_time, Time
239
+
240
+ property :rest_temperature, Temperature, :required => true
241
+ property :rest_time, Time, :required => true
242
+
243
+ def cleanup
244
+ self.index = mash_schedule.mash_steps.index(self)+1
245
+ self.infusion_volume = display_infuse_amt.present? ? display_infuse_amt.u : "#{uncast_infusion_volume} l".u unless !uncast_infusion_volume.present? or uncast_infusion_volume == 0
246
+ if infusion_temperature.present?
247
+ self.infusion_temperature
248
+ self.infusion_temperature = infusion_temperature.unitless? ? infusion_temperature*"1 C".u : infusion_temperature
249
+ end
250
+ self.rest_temperature = "#{uncast_rest_temperature} dC".u
251
+ end
252
+
253
+ end
254
+
255
+ class BeerXML::MashSchedule < Brewser::MashSchedule
256
+ include ROXML
257
+
258
+ xml_name "MASH"
259
+ xml_convention :upcase
260
+ xml_reader :name
261
+ xml_reader :description, :from => "NOTES"
262
+
263
+ xml_reader :display_grain_temp
264
+ xml_reader :uncast_grain_temp, :from => "GRAIN_TEMP"
265
+
266
+ xml_reader :display_sparge_temp
267
+ xml_reader :uncast_sparge_temp, :from => "SPARGE_TEMP"
268
+
269
+ xml_attr :mash_steps, :as => [BeerXML::MashStep], :in => "MASH_STEPS"
270
+
271
+ def cleanup
272
+ mash_steps.each do |m|
273
+ m.cleanup
274
+ end
275
+ self.grain_temp = display_grain_temp.present? ? display_grain_temp.u : "#{uncast_grain_temp} dC".u
276
+ self.sparge_temp = display_sparge_temp.present? ? display_sparge_temp.u : "#{uncast_sparge_temp} dC".u
277
+ end
278
+
279
+ end
280
+
281
+ class BeerXML::FermentationStep < Brewser::FermentationStep
282
+
283
+ end
284
+
285
+ class BeerXML::FermentationSchedule < Brewser::FermentationSchedule
286
+
287
+ end
288
+
289
+ class BeerXML::WaterProfile < Brewser::WaterProfile
290
+ include ROXML
291
+
292
+ xml_name "WATER"
293
+ xml_convention :upcase
294
+ xml_reader :name, :from => "NAME"
295
+ xml_reader(:calcium, :from => "CALCIUM") {|x| x.to_f }
296
+ xml_reader(:magnesium, :from => "MAGNESIUM") {|x| x.to_f }
297
+ xml_reader(:sodium, :from => "SODIUM") {|x| x.to_f }
298
+ xml_reader(:chloride, :from => "CHLORIDE") {|x| x.to_f }
299
+ xml_reader(:sulfates, :from => "SULFATE") {|x| x.to_f }
300
+ xml_reader(:bicarbonate, :from => "BICARBONATE") {|x| x.to_f }
301
+ xml_reader(:alkalinity, :from => "ALKALINITY") {|x| x.to_f }
302
+ xml_reader(:ph, :from => "PH") {|x| x.to_f }
303
+ xml_reader :description, :from => "NOTES"
304
+
305
+ def cleanup
306
+ # nothing to do here
307
+ end
308
+ end
309
+
310
+
311
+ class BeerXML::Style < Brewser::Style
312
+ include ROXML
313
+
314
+ xml_name "STYLE"
315
+ xml_convention :upcase
316
+ xml_reader :name
317
+ xml_reader :category
318
+ xml_reader :category_number
319
+ xml_reader :style_letter
320
+ xml_reader :style_guide
321
+ xml_reader(:type) { |x|
322
+ x="Hybrid" if x=="Mixed"
323
+ x
324
+ }
325
+ xml_attr :og_min
326
+ xml_attr :og_max
327
+ xml_attr :fg_min
328
+ xml_attr :fg_max
329
+ xml_attr :ibu_min
330
+ xml_attr :ibu_max
331
+ xml_attr :color_min
332
+ xml_attr :color_max
333
+
334
+ xml_attr :carb_min
335
+ xml_attr :carb_max
336
+ xml_attr :abv_min
337
+ xml_attr :abv_max
338
+
339
+ xml_attr :notes
340
+ xml_attr :profile
341
+ xml_attr :ingredients
342
+ xml_attr :examples
343
+
344
+ def cleanup
345
+ # nothing to do here
346
+ end
347
+ end
348
+
349
+ class BeerXML::Recipe < Brewser::Recipe
350
+ include ROXML
351
+
352
+ xml_name "RECIPE"
353
+ xml_convention :upcase
354
+ xml_reader :date_created, :from => "DATE"
355
+ xml_reader :name
356
+ xml_reader :method, :from => "TYPE"
357
+
358
+ xml_attr :style, :as => BeerXML::Style
359
+ xml_attr :mash_schedule, :as => BeerXML::MashSchedule
360
+ xml_attr :water_profile, :as => BeerXML::WaterProfile, :in => "WATERS"
361
+
362
+ xml_reader :brewer
363
+
364
+ xml_reader :display_batch_size
365
+ xml_reader :uncast_batch_size, :from => "BATCH_SIZE"
366
+
367
+ xml_reader :display_boil_size
368
+ xml_reader :uncast_boil_size, :from => "BOIL_SIZE"
369
+
370
+ xml_reader(:boil_time) { |x| "#{x} min".u }
371
+
372
+ xml_reader :recipe_efficiency, :from => "EFFICIENCY", :as => Float
373
+ xml_reader :carbonation_level, :from => "CARBONATION", :as => Float
374
+
375
+ xml_reader(:estimated_og, :from => "EST_OG") {|x| x.to_f }
376
+ xml_reader(:estimated_fg, :from => "EST_FG") {|x| x.to_f }
377
+ xml_reader(:estimated_color, :from => "EST_COLOR") {|x| x.to_f }
378
+ xml_reader(:estimated_ibu, :from => "IBU") {|x| x.to_f }
379
+ xml_reader(:estimated_abv, :from => "EST_ABV") {|x| x.to_f }
380
+
381
+
382
+ xml_attr :hops, :as => [BeerXML::Hop], :in => "HOPS"
383
+ xml_attr :fermentables, :as => [BeerXML::Fermentable], :in => "FERMENTABLES"
384
+ xml_attr :additives, :as => [BeerXML::Additive], :in => "MISCS"
385
+ xml_attr :yeasts, :as => [BeerXML::Yeast], :in => "YEASTS"
386
+
387
+ xml_reader :description, :from => "NOTES"
388
+
389
+ xml_reader :primary_age, :from => "PRIMARY_AGE"
390
+ xml_reader :display_primary_temp
391
+ xml_reader :uncast_primary_temp, :from => "PRIMARY_TEMP"
392
+
393
+ xml_reader :secondary_age, :from => "SECONDARY_AGE"
394
+ xml_reader :display_secondary_temp
395
+ xml_reader :uncast_secondary_temp, :from => "SECONDARY_TEMP"
396
+
397
+ xml_reader :tertiary_age, :from => "TERTIARY_AGE"
398
+ xml_reader :display_tertiary_temp
399
+ xml_reader :uncast_teritary_temp, :from => "TERTIARY_TEMP"
400
+
401
+ xml_reader :age, :from => "AGE"
402
+ xml_reader :display_temp, :from => "DISPLAY_AGE_TEMP"
403
+ xml_reader :uncast_age_temp, :from => "TEMP"
404
+
405
+ def cleanup
406
+ self.recipe_volume = display_batch_size.present? ? display_batch_size.u : "#{uncast_batch_size} l".u
407
+ self.boil_volume = display_boil_size.present? ? display_boil_size.u : "#{uncast_boil_size} l".u
408
+ self.type = style.type || "Other"
409
+ mash_schedule.cleanup
410
+ hops.each(&:cleanup)
411
+ fermentables.each(&:cleanup)
412
+ additives.each(&:cleanup)
413
+ yeasts.each(&:cleanup)
414
+ current_index = 1
415
+ self.fermentation_schedule = BeerXML::FermentationSchedule.create
416
+ ["primary_age","secondary_age","tertiary_age","age"].each do |stage|
417
+ if self.send(stage).present? && self.send(stage).to_i > 0
418
+ current_step = BeerXML::FermentationStep.new
419
+ current_step.name = stage.split("_")[0].capitalize
420
+ current_step.purpose = current_step.name
421
+ current_step.purpose = "Conditioning" if current_step.purpose == "Age"
422
+ current_step.index = current_index
423
+ current_index += 1
424
+ current_step.time = "#{self.send(stage)} days".u
425
+ display = "display_#{stage.gsub("age","temp")}"
426
+ uncast = "uncast_#{stage.gsub("age","temp")}"
427
+ current_step.temperature = self.send(display).present? ? self.send(display).u : "#{self.send(uncast)} dC".u
428
+ self.fermentation_schedule.fermentation_steps.push current_step
429
+ end
430
+ end
431
+
432
+ end
433
+ end