brewser 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +25 -1
- data/lib/brewser/engines/beerxml.rb +2 -3
- data/lib/brewser/engines/beerxml2.rb +1 -3
- data/lib/brewser/engines/brewson.rb +10 -1
- data/lib/brewser/engines/promash_rec.rb +52 -32
- data/lib/brewser/model/additive.rb +26 -0
- data/lib/brewser/model/base.rb +4 -16
- data/lib/brewser/model/fermentable.rb +42 -1
- data/lib/brewser/model/fermentation_schedule.rb +17 -0
- data/lib/brewser/model/fermentation_steps.rb +21 -0
- data/lib/brewser/model/hop.rb +44 -0
- data/lib/brewser/model/mash_schedule.rb +25 -1
- data/lib/brewser/model/mash_steps.rb +33 -0
- data/lib/brewser/model/recipe.rb +63 -0
- data/lib/brewser/model/style.rb +22 -0
- data/lib/brewser/model/units.rb +6 -1
- data/lib/brewser/model/water_profile.rb +30 -1
- data/lib/brewser/model/yeast.rb +37 -0
- data/lib/brewser/version.rb +1 -1
- data/spec/basic_spec.rb +1 -1
- data/spec/beerxml_spec.rb +0 -1
- data/spec/brewson_spec.rb +207 -0
- data/spec/promash_rec_spec.rb +226 -0
- data/spec/promash_spec.rb +1 -108
- metadata +48 -28
data/README.md
CHANGED
@@ -3,7 +3,7 @@ brewser
|
|
3
3
|
|
4
4
|
Brewser is a ruby library for parsing and generating serialized brewing data
|
5
5
|
|
6
|
-
Currently brewser is
|
6
|
+
Currently brewser is early in development but will eventually support the following input formats:
|
7
7
|
* [BeerXML (v1, v2)](http://beerxml.org)
|
8
8
|
* BrewSON - Brewser Recipe and Batches in JSON
|
9
9
|
* [ProMash (.rec and text exports)](http://www.promash.com)
|
@@ -11,3 +11,27 @@ Currently brewser is very early in development but will eventually support the f
|
|
11
11
|
Input files are deserialized into a common object model for consumption. Brewser supports these output formats:
|
12
12
|
* BeerXML
|
13
13
|
* BrewSON
|
14
|
+
|
15
|
+
# Status
|
16
|
+
|
17
|
+
Currently, brewser can import BeerXML v1, ProMash .rec files as well as ProMash Recipe Reports (Text files). Each
|
18
|
+
file format contains different levels of details. As a result there is no uniformity amongst the various format. BeerXML v1
|
19
|
+
is at this time the most complete serialized format.
|
20
|
+
|
21
|
+
# Installation
|
22
|
+
|
23
|
+
Just add brewser to your Gemfile
|
24
|
+
|
25
|
+
gem 'brewser'
|
26
|
+
|
27
|
+
and run bundle install
|
28
|
+
|
29
|
+
# Using
|
30
|
+
|
31
|
+
Brewser will attempt to identify the input file by content and delegate processing to the correct engine. ProMash .rec files are not easy to identify, as a result this is the last engine attempted and it will try to decode any file you give it. This is likely to result in garbage if the file is not a properly formatted .rec file.
|
32
|
+
|
33
|
+
To load a BeerXML v1 file:
|
34
|
+
|
35
|
+
Brewser.parse(File.read("samples/beerxmlv1/recipes.xml"))
|
36
|
+
|
37
|
+
This will return an array of BeerXML::Recipe objects.
|
@@ -18,8 +18,8 @@ class BeerXML < Brewser::Engine
|
|
18
18
|
("BeerXML::#{inner.node_name.downcase.camelcase}".constantize).from_xml(inner)
|
19
19
|
end
|
20
20
|
return cleanup(objects)
|
21
|
-
|
22
|
-
|
21
|
+
rescue
|
22
|
+
raise Error, "BeerXML engine encountered an issue and can not continue"
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -117,7 +117,6 @@ class BeerXML::Fermentable < Brewser::Fermentable
|
|
117
117
|
def cleanup
|
118
118
|
self.amount = display_amount.present? ? display_amount.u : "#{uncast_amount} kg".u
|
119
119
|
self.potential = uncast_potential.present? ? uncast_potential.to_f : 1+(46*(yield_percent/100))/1000
|
120
|
-
self.ppg = (potential-1)*1000
|
121
120
|
end
|
122
121
|
|
123
122
|
end
|
@@ -7,10 +7,19 @@ class BrewSON < Brewser::Engine
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def deserialize(string_or_io)
|
10
|
+
begin
|
11
|
+
JSON.parse(string_or_io)
|
12
|
+
rescue
|
13
|
+
raise Error, "BrewSON engine encountered an issue and can not continue"
|
14
|
+
end
|
10
15
|
end
|
11
16
|
|
12
17
|
def serialize(brewser_model)
|
13
|
-
|
18
|
+
begin
|
19
|
+
JSON.generate(brewser_model)
|
20
|
+
rescue
|
21
|
+
raise Error, "BrewSON engine encountered an issue and can not continue"
|
22
|
+
end
|
14
23
|
end
|
15
24
|
|
16
25
|
end
|
@@ -32,7 +32,8 @@ class ProMashRec < Brewser::Engine
|
|
32
32
|
float :humulene
|
33
33
|
float :caryphylene
|
34
34
|
int8 :type
|
35
|
-
|
35
|
+
bit4 :form
|
36
|
+
bit4 :unknown
|
36
37
|
float :storage_factor
|
37
38
|
string :taste_notes, :length => 155, :trim_padding => true
|
38
39
|
string :origin, :length => 55, :trim_padding => true
|
@@ -110,26 +111,29 @@ class ProMashRec < Brewser::Engine
|
|
110
111
|
skip length: 8
|
111
112
|
end
|
112
113
|
|
113
|
-
class MashStep < RecEntry
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
class MashSchedule < RecEntry
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
114
|
+
# class MashStep < RecEntry
|
115
|
+
# string :name, :length => 255, :trim_padding => true
|
116
|
+
# int8 :type
|
117
|
+
# int32 :start_temp
|
118
|
+
# int32 :stop_temp
|
119
|
+
# int32 :step_temp
|
120
|
+
# int32 :rest_time
|
121
|
+
# int32 :step_time
|
122
|
+
# float :thickness
|
123
|
+
# float :amount
|
124
|
+
# skip length: 8
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# class MashSchedule < RecEntry
|
128
|
+
# count_bytes_remaining :bytes_remaining
|
129
|
+
# string :skipped, :length => lambda { bytes_remaining - 14888 }
|
130
|
+
# int32 :_steps_count
|
131
|
+
# int32 :grain_temp
|
132
|
+
# skip length: 4
|
133
|
+
# array :mash_steps, :type => :mash_step, :read_until => lambda { index == 50 }
|
134
|
+
# string :name, :length => 255, :trim_padding => true
|
135
|
+
# int8 :end_byte
|
136
|
+
# end
|
133
137
|
|
134
138
|
class SimpleSchedule < RecEntry
|
135
139
|
int32 :num_steps
|
@@ -194,9 +198,9 @@ class ProMashRec < Brewser::Engine
|
|
194
198
|
yeast :yeast
|
195
199
|
water_profile :water_profile
|
196
200
|
simple_schedule :simple_schedule
|
197
|
-
string :notes, :length =>
|
198
|
-
string :awards, :length =>
|
199
|
-
mash_schedule :mash_schedule
|
201
|
+
string :notes, :length => 4028, :trim_padding => true
|
202
|
+
string :awards, :length => 2096, :trim_padding => true
|
203
|
+
#mash_schedule :mash_schedule
|
200
204
|
end
|
201
205
|
end
|
202
206
|
|
@@ -206,7 +210,7 @@ end
|
|
206
210
|
class ProMashRec::Hop < Brewser::Hop
|
207
211
|
|
208
212
|
@@hop_types = {0 => "Both", 1 => "Bittering", 2 => "Aroma"}
|
209
|
-
@@hop_forms = {
|
213
|
+
@@hop_forms = {0 => "Whole", 1 => "Plug", 2 => "Pellet"}
|
210
214
|
|
211
215
|
# Pellet 21, 23
|
212
216
|
def from_promash(hop,time)
|
@@ -282,7 +286,7 @@ end
|
|
282
286
|
class ProMashRec::Yeast < Brewser::Yeast
|
283
287
|
|
284
288
|
def from_promash(y)
|
285
|
-
self.name = y.name.split("\x00")[0]
|
289
|
+
self.name = y.name.split("\x00")[0].strip
|
286
290
|
self.supplier = y.supplier
|
287
291
|
self.catalog = y.catalog
|
288
292
|
self.attenuation = (y.aa_high + y.aa_low)/2
|
@@ -297,6 +301,8 @@ end
|
|
297
301
|
|
298
302
|
class ProMashRec::MashStep < Brewser::MashStep
|
299
303
|
|
304
|
+
@@step_types = { 0 => "Infusion", 1 => "Direct", 2 => "Decoction" }
|
305
|
+
|
300
306
|
def from_simple(idx, name, rest_temp, rest_time)
|
301
307
|
self.index = idx
|
302
308
|
self.name = name
|
@@ -306,11 +312,29 @@ class ProMashRec::MashStep < Brewser::MashStep
|
|
306
312
|
return self
|
307
313
|
end
|
308
314
|
|
315
|
+
def from_promash(step,idx=nil)
|
316
|
+
self.name = step.name
|
317
|
+
self.index = idx
|
318
|
+
self.type = @@step_types[step.type]
|
319
|
+
self.ramp_time = "#{step.step_time} min".u
|
320
|
+
self.rest_temperature = "#{step.start_temp} dF".u
|
321
|
+
self.rest_time = "#{step.rest_time} min".u
|
322
|
+
self.infusion_temperature = "#{step.step_temp} dF".u unless step.step_temp.blank?
|
323
|
+
self.infusion_amount = "#{step.amount} qts".u unless step.amount.blank?
|
324
|
+
|
325
|
+
return self
|
326
|
+
end
|
327
|
+
|
309
328
|
end
|
310
329
|
|
311
330
|
class ProMashRec::MashSchedule < Brewser::MashSchedule
|
312
331
|
|
313
332
|
def from_promash(mash)
|
333
|
+
self.name = mash.name
|
334
|
+
self.grain_temp = "#{mash.grain_temp} dF".u
|
335
|
+
mash.steps.each do |step, idx|
|
336
|
+
self.mash_steps.push ProMashRec::MashStep.new.from_promash(step,idx)
|
337
|
+
end
|
314
338
|
|
315
339
|
return self
|
316
340
|
end
|
@@ -410,14 +434,10 @@ class ProMashRec::Recipe < Brewser::Recipe
|
|
410
434
|
end
|
411
435
|
self.yeasts.push ProMashRec::Yeast.new.from_promash(rec.yeast)
|
412
436
|
self.water_profile = ProMashRec::WaterProfile.new.from_promash(rec.water_profile)
|
413
|
-
|
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
|
437
|
+
self.mash_schedule = ProMashRec::SimpleSchedule.new.from_promash(rec.simple_schedule)
|
418
438
|
self.description = rec.notes
|
419
439
|
self.type = self.style.type
|
420
|
-
|
440
|
+
|
421
441
|
return self
|
422
442
|
end
|
423
443
|
|
@@ -10,5 +10,31 @@ module Brewser
|
|
10
10
|
property :amount, WeightOrVolume, :required => true
|
11
11
|
|
12
12
|
property :use_for, String
|
13
|
+
|
14
|
+
def self.json_create(o)
|
15
|
+
a = self.new
|
16
|
+
a.name = o['name']
|
17
|
+
a.description = o['description']
|
18
|
+
a.type = o['type']
|
19
|
+
a.added_when = o['added_when']
|
20
|
+
a.time = o['time'].u unless o['time'].blank?
|
21
|
+
a.amount = o['amount'].u unless o['amount'].blank?
|
22
|
+
a.use_for = o['use_for']
|
23
|
+
|
24
|
+
return a
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_json(options={})
|
28
|
+
{
|
29
|
+
JSON.create_id => "Brewser::Additive",
|
30
|
+
'name' => name,
|
31
|
+
'description' => description,
|
32
|
+
'type' => type,
|
33
|
+
'added_when' => added_when,
|
34
|
+
'time' => time.to_s, 'amount' => amount.to_s,
|
35
|
+
'use_for' => use_for
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
13
39
|
end
|
14
40
|
end
|
data/lib/brewser/model/base.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
module Brewser
|
2
|
-
|
2
|
+
|
3
3
|
class Model
|
4
4
|
require 'dm-core'
|
5
5
|
require 'dm-validations'
|
6
|
+
require 'dm-serializer'
|
7
|
+
|
8
|
+
require "msgpack"
|
6
9
|
require 'active_support/inflector'
|
7
10
|
|
8
11
|
include DataMapper::Resource
|
@@ -14,21 +17,6 @@ module Brewser
|
|
14
17
|
def self.auto_migrate_down!(rep);end
|
15
18
|
def self.auto_migrate_up!(rep);end
|
16
19
|
def self.auto_upgrade!(rep);end
|
17
|
-
|
18
|
-
def deep_json
|
19
|
-
h = {}
|
20
|
-
instance_variables.each do |e|
|
21
|
-
key = e[1..-1]
|
22
|
-
next if ["roxml_references", "_persistence_state", "_key"].include? key
|
23
|
-
o = instance_variable_get e.to_sym
|
24
|
-
h[key] = (o.respond_to? :deep_json) ? o.deep_json : o;
|
25
|
-
end
|
26
|
-
h
|
27
|
-
end
|
28
|
-
|
29
|
-
def to_json *a
|
30
|
-
deep_json.to_json *a
|
31
|
-
end
|
32
20
|
|
33
21
|
def as_brewson
|
34
22
|
BrewSON.serialize(self)
|
@@ -10,7 +10,6 @@ module Brewser
|
|
10
10
|
property :type, String, :set => ['Grain', 'Sugar', 'Extract', 'Dry Extract', 'Adjunct'], :required => true
|
11
11
|
property :yield_percent, Float
|
12
12
|
property :potential, Float, :required => true
|
13
|
-
property :ppg, Integer
|
14
13
|
|
15
14
|
property :color, Float, :required => true
|
16
15
|
|
@@ -24,5 +23,47 @@ module Brewser
|
|
24
23
|
property :max_in_batch, Float
|
25
24
|
property :recommend_mash?, Boolean
|
26
25
|
property :ibu_gal_per_lb, Float
|
26
|
+
|
27
|
+
def ppg
|
28
|
+
return 0 if potential.blank?
|
29
|
+
(potential-1)*1000
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.json_create(o)
|
33
|
+
a = self.new
|
34
|
+
a.name = o['name']
|
35
|
+
a.origin = o['origin']
|
36
|
+
a.supplier = o['supplier']
|
37
|
+
a.description = o['description']
|
38
|
+
a.type = o['type']
|
39
|
+
a.potential = o['potential']
|
40
|
+
a.color = o['color']
|
41
|
+
a.amount = o['amount'].u unless o['amount'].blank?
|
42
|
+
a.late_addition = o['added_late']
|
43
|
+
a.coarse_fine_diff = o['coarse_fine_diff']
|
44
|
+
a.moisture = o['moisture']
|
45
|
+
a.diastatic_power = o['diastatic_power']
|
46
|
+
a.protein = o['protein']
|
47
|
+
a.max_in_batch = o['max_in_batch']
|
48
|
+
a.origin = o['origin']
|
49
|
+
a.recommend_mash = o['recommend_mash']
|
50
|
+
a.ibu_gal_per_lb = o['ibu_gal_per_lb']
|
51
|
+
|
52
|
+
return a
|
53
|
+
end
|
54
|
+
|
55
|
+
def as_json(options={})
|
56
|
+
{
|
57
|
+
JSON.create_id => "Brewser::Fermentable",
|
58
|
+
'name' => name, 'origin' => origin,
|
59
|
+
'supplier' => supplier, 'description' => description,
|
60
|
+
'type' => type, 'ppg' => ppg, 'potential' => potential,
|
61
|
+
'color' => color, 'amount' => amount.to_s, 'added_late' => late_addition?,
|
62
|
+
'coarse_fine_diff' => coarse_fine_diff, 'moisture' => moisture,
|
63
|
+
'diastatic_power' => diastatic_power, 'protein' => protein, 'max_in_batch' => max_in_batch,
|
64
|
+
'recommend_mash' => recommend_mash?, 'ibu_gal_per_lb' => ibu_gal_per_lb
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
27
68
|
end
|
28
69
|
end
|
@@ -3,5 +3,22 @@ module Brewser
|
|
3
3
|
belongs_to :recipe
|
4
4
|
|
5
5
|
has n, :fermentation_steps
|
6
|
+
|
7
|
+
def self.json_create(o)
|
8
|
+
a = self.new
|
9
|
+
o['fermentation_steps'].each do |step|
|
10
|
+
a.fermentation_steps.push step
|
11
|
+
end unless o['fermentation_steps'].nil?
|
12
|
+
|
13
|
+
return a
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json(options={})
|
17
|
+
{
|
18
|
+
JSON.create_id => "Brewser::FermentationSchedule",
|
19
|
+
'fermentation_steps' => fermentation_steps.to_a
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
6
23
|
end
|
7
24
|
end
|
@@ -8,5 +8,26 @@ module Brewser
|
|
8
8
|
property :index, Integer
|
9
9
|
property :time, TimeInDays
|
10
10
|
property :temperature, Temperature
|
11
|
+
|
12
|
+
def self.json_create(o)
|
13
|
+
a = self.new
|
14
|
+
a.name = o['name']
|
15
|
+
a.purpose = o['purpose']
|
16
|
+
a.index = o['index']
|
17
|
+
a.time = o['time'].u unless o['time'].blank?
|
18
|
+
a.temperature = o['temperature'].u unless o['temperature'].blank?
|
19
|
+
|
20
|
+
return a
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_json(options={})
|
24
|
+
{
|
25
|
+
JSON.create_id=> "Brewser::FermentationStep",
|
26
|
+
'name' => name, 'purpose' => purpose,
|
27
|
+
'index' => index,
|
28
|
+
'time' => time.to_s, 'temperature' => temperature.to_s
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
11
32
|
end
|
12
33
|
end
|
data/lib/brewser/model/hop.rb
CHANGED
@@ -23,5 +23,49 @@ module Brewser
|
|
23
23
|
property :myrcene, Float
|
24
24
|
property :farnsene, Float
|
25
25
|
property :total_oil, Float
|
26
|
+
|
27
|
+
def self.json_create(o)
|
28
|
+
a = self.new
|
29
|
+
a.name = o['name']
|
30
|
+
a.origin = o['origin']
|
31
|
+
a.description = o['description']
|
32
|
+
a.type = o['type']
|
33
|
+
a.alpha_acids = o['alpha_acids']
|
34
|
+
a.beta_acids = o['beta_acids']
|
35
|
+
a.added_when = o['added_when']
|
36
|
+
a.time = o['time'].u unless o['time'].blank?
|
37
|
+
a.amount = o['amount'].u unless o['amount'].blank?
|
38
|
+
a.form = o['form']
|
39
|
+
a.storageability = o['storageability']
|
40
|
+
a.substitutes = o['substitutes']
|
41
|
+
a.humulene = o['humulene']
|
42
|
+
a.caryophyllene = o['caryophyllene']
|
43
|
+
a.cohumulone = o['cohumulone']
|
44
|
+
a.myrcene = o['myrcene']
|
45
|
+
a.farnsene = o['farnsene']
|
46
|
+
a.total_oil = o['total_oil']
|
47
|
+
|
48
|
+
return a
|
49
|
+
end
|
50
|
+
|
51
|
+
def as_json(options={})
|
52
|
+
{
|
53
|
+
JSON.create_id=> "Brewser::Hop",
|
54
|
+
'name' => name,
|
55
|
+
'origin' => origin,
|
56
|
+
'description' => description,
|
57
|
+
'type' => type,
|
58
|
+
'alpha_acids' => alpha_acids,
|
59
|
+
'beta_acids' => beta_acids,
|
60
|
+
'added_when' => added_when,
|
61
|
+
'time' => time.to_s, 'amount' => amount.to_s,
|
62
|
+
'form' => form, 'storageability' => storageability,
|
63
|
+
'substitutes' => substitutes,
|
64
|
+
'humulene' => humulene, 'caryophyllene' => caryophyllene,
|
65
|
+
'cohumulone' => cohumulone, 'myrcene' => myrcene,
|
66
|
+
'farnsene' => farnsene, 'total_oil' => total_oil
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
26
70
|
end
|
27
71
|
end
|