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,50 @@
|
|
1
|
+
module Brewser
|
2
|
+
class Recipe < Model
|
3
|
+
belongs_to :batch
|
4
|
+
|
5
|
+
property :date_created, Date
|
6
|
+
property :name, String, :required => true
|
7
|
+
property :description, String, :length => 65535
|
8
|
+
|
9
|
+
property :type, String, :set => ['Ale', 'Lager', 'Wheat', 'Cider', 'Mead', 'Hybrid']
|
10
|
+
property :method, String, :set => ['Extract', 'Partial Mash', 'All Grain'], :required => true
|
11
|
+
|
12
|
+
has 1, :style
|
13
|
+
validates_presence_of :style
|
14
|
+
|
15
|
+
property :brewer, String
|
16
|
+
property :recipe_volume, Volume, :required => true
|
17
|
+
property :boil_volume, Volume, :required => true
|
18
|
+
property :boil_time, Time, :required => true
|
19
|
+
property :recipe_efficiency, Float
|
20
|
+
validates_presence_of :recipe_efficiency, :if => proc { |t| t.method != 'Extract' }
|
21
|
+
|
22
|
+
has n, :hops
|
23
|
+
has n, :fermentables
|
24
|
+
has n, :additives
|
25
|
+
has n, :yeasts
|
26
|
+
|
27
|
+
has 1, :mash_schedule
|
28
|
+
has 1, :fermentation_schedule
|
29
|
+
has 1, :water_profile
|
30
|
+
|
31
|
+
property :asst_brewer, String
|
32
|
+
property :taste_notes, String, :length => 65535
|
33
|
+
property :taste_rating, Float
|
34
|
+
|
35
|
+
property :estimated_og, Float
|
36
|
+
property :estimated_fg, Float
|
37
|
+
property :estimated_color, Float
|
38
|
+
property :estimated_ibu, Float
|
39
|
+
property :estimated_abv, Float
|
40
|
+
|
41
|
+
property :source, String, :length => 65535
|
42
|
+
property :url, String, :length => 65535
|
43
|
+
|
44
|
+
property :carbonation_level, Float
|
45
|
+
|
46
|
+
validates_presence_of :mash_schedule, :if => proc { |t| t.method != 'Extract' }
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Brewser
|
2
|
+
class Style < Brewser::Model
|
3
|
+
belongs_to :recipe
|
4
|
+
|
5
|
+
property :name, String, :required => true
|
6
|
+
property :category, String, :required => true
|
7
|
+
property :category_number, String, :required => true
|
8
|
+
property :style_letter, String, :required => true
|
9
|
+
property :style_guide, String, :required => true
|
10
|
+
property :type, String, :set => %w(Lager Ale Mead Wheat Mixed Cider)
|
11
|
+
property :og_min, Float
|
12
|
+
property :og_max, Float
|
13
|
+
property :fg_min, Float
|
14
|
+
property :fg_max, Float
|
15
|
+
property :ibu_min, Float
|
16
|
+
property :ibu_max, Float
|
17
|
+
property :color_min, Float
|
18
|
+
property :color_max, Float
|
19
|
+
|
20
|
+
property :carb_min, Float
|
21
|
+
property :carb_max, Float
|
22
|
+
property :abv_min, Float
|
23
|
+
property :abv_max, Float
|
24
|
+
property :notes, String, :length => 65535
|
25
|
+
property :profile, String, :length => 65535
|
26
|
+
property :ingredients, String, :length => 65535
|
27
|
+
property :examples, String, :length => 65535
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'ruby-units'
|
2
|
+
|
3
|
+
module Brewser::Model::Units
|
4
|
+
|
5
|
+
class Units < DataMapper::Property::String
|
6
|
+
|
7
|
+
# Declare this property as custom
|
8
|
+
def custom?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
# Compare the values kind (mass, volume, etc) to the object classes kind_of
|
13
|
+
#
|
14
|
+
# @param [Unit] value value to be checked
|
15
|
+
# @return [Boolean] returns true if value is of the correct kind
|
16
|
+
def valid_kind?(value)
|
17
|
+
value.kind == kind_of
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
def primitive?(value)
|
22
|
+
value.is_a?(Unit) && valid_kind?(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def load(value)
|
26
|
+
return if value.nil?
|
27
|
+
if !value.u.unitless?
|
28
|
+
raise(ArgumentError, "#{value.inspect} is not a #{kind_of}") unless value.u.kind == kind_of
|
29
|
+
value.u
|
30
|
+
else
|
31
|
+
"#{value} #{base_unit}".u
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def dump(value)
|
36
|
+
return if value.nil?
|
37
|
+
value.u.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def typecast_to_primitive(value)
|
41
|
+
load(value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Represents a weight. Default scale is kilograms, since that's what
|
46
|
+
# BeerXML uses. But can handle conversion/display of other units.
|
47
|
+
class Weight < Units
|
48
|
+
def kind_of
|
49
|
+
:mass
|
50
|
+
end
|
51
|
+
def base_unit
|
52
|
+
'kg'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# A volume, in liters.
|
57
|
+
class Volume < Units
|
58
|
+
def kind_of
|
59
|
+
:volume
|
60
|
+
end
|
61
|
+
def base_unit
|
62
|
+
'l'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class WeightOrVolume < Units
|
67
|
+
|
68
|
+
def valid_kind?(value)
|
69
|
+
value.kind == :mass || value.kind == :volume
|
70
|
+
end
|
71
|
+
def base_unit
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# A temperature, in deg C.
|
76
|
+
class Temperature < Units
|
77
|
+
def kind_of
|
78
|
+
:temperature
|
79
|
+
end
|
80
|
+
def base_unit
|
81
|
+
'dC'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# A time, in minutes.
|
86
|
+
class Time < Units
|
87
|
+
def kind_of
|
88
|
+
:time
|
89
|
+
end
|
90
|
+
def base_unit
|
91
|
+
'min'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Time, but in days.
|
96
|
+
class TimeInDays < Time
|
97
|
+
def base_unit
|
98
|
+
'days'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Brewser
|
2
|
+
class WaterProfile < Model
|
3
|
+
belongs_to :recipe
|
4
|
+
|
5
|
+
property :name, String, :required => true
|
6
|
+
|
7
|
+
property :calcium, Float, :required => true
|
8
|
+
property :magnesium, Float, :required => true
|
9
|
+
property :sodium, Float, :required => true
|
10
|
+
property :chloride, Float, :required => true
|
11
|
+
property :sulfates, Float, :required => true
|
12
|
+
property :bicarbonate, Float
|
13
|
+
property :alkalinity, Float
|
14
|
+
property :ph, Float
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Brewser
|
2
|
+
class Yeast < Model
|
3
|
+
belongs_to :recipe
|
4
|
+
|
5
|
+
property :name, String, :required => true
|
6
|
+
property :description, String, :length => 65535
|
7
|
+
|
8
|
+
property :type, String, :set => ['Ale', 'Lager', 'Wheat', 'Wine', 'Champagne'], :required => true
|
9
|
+
|
10
|
+
property :supplier, String
|
11
|
+
property :catalog, String
|
12
|
+
|
13
|
+
property :min_temperature, Temperature
|
14
|
+
property :max_temperature, Temperature
|
15
|
+
property :flocculation, String, :set => ['Low', 'Medium', 'High', 'Very High']
|
16
|
+
property :attenuation, Float
|
17
|
+
property :best_for, String
|
18
|
+
property :max_reuse, Integer
|
19
|
+
|
20
|
+
# Recipes
|
21
|
+
property :add_to_secondary?, Boolean
|
22
|
+
property :form, String, :set => ['Liquid', 'Dry', 'Slant', 'Culture'], :required => true
|
23
|
+
property :amount, WeightOrVolume, :required => true
|
24
|
+
|
25
|
+
# These are applicable only in Batches
|
26
|
+
property :times_cultured, Integer
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'ruby-units'
|
2
|
+
|
3
|
+
Unit.redefine!('celsius') do |celsius|
|
4
|
+
celsius.aliases = %w{degC dC C celsius centigrade}
|
5
|
+
end
|
6
|
+
|
7
|
+
Unit.redefine!('fahrenheit') do |fahrenheit|
|
8
|
+
fahrenheit.aliases = %w{degF dF F fahrenheit}
|
9
|
+
end
|
10
|
+
|
11
|
+
Unit.redefine!('gram') do |gram|
|
12
|
+
gram.aliases = %w{g gm gram grams gramme grammes}
|
13
|
+
end
|
14
|
+
|
15
|
+
Unit.define('barrel') do |barrel|
|
16
|
+
barrel.definition = Unit('31 gal')
|
17
|
+
barrel.aliases = %w{bbl bbls barrel barrels}
|
18
|
+
end
|
19
|
+
|
20
|
+
Unit.define('keg') do |keg|
|
21
|
+
keg.definition = Unit('1/2 barrel')
|
22
|
+
keg.aliases = %w{keg kegs}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add convienience method
|
26
|
+
class Unit
|
27
|
+
# returns the scalar value convert to other units
|
28
|
+
def scalar_in(other)
|
29
|
+
to(other).scalar.to_f
|
30
|
+
end
|
31
|
+
end
|
data/spec/basic_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Basic spec" do
|
4
|
+
|
5
|
+
it "should be able to identifiy json as BrewSON" do
|
6
|
+
Brewser.identify('{"test":"json"}').should == BrewSON
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be able to identifiy xml as BeerXML" do
|
10
|
+
Brewser.identify(read_file('beerxmlv1/recipes.xml')).should == BeerXML
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be able to identifiy xml as BeerXML2" do
|
14
|
+
Brewser.identify(read_file('beerxmlv2/belgian_white.xml')).should == BeerXML2
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be able to identify text as ProMash Txt" do
|
18
|
+
Brewser.identify(read_file('promash/PumpkinAle.txt')).should == ProMashTxt
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should raise an error if it cannot identify content" do
|
22
|
+
Brewser.identify("Some unknown content").should == ProMashRec
|
23
|
+
end
|
24
|
+
|
25
|
+
context "BeerXML v1" do
|
26
|
+
|
27
|
+
it "should extract multiple recipes" do
|
28
|
+
recipes = Brewser.parse(read_file('beerxmlv1/recipes.xml'))
|
29
|
+
recipes.count.should == 4
|
30
|
+
recipes.first.class.should == BeerXML::Recipe
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should raise an error when encountered" do
|
34
|
+
lambda { Brewser.parse(read_file('beerxmlv1/broken.xml')) }.should raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "BeerXML tests" do
|
4
|
+
|
5
|
+
context "BeerXML v1" do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
@recipe = Brewser.parse(read_file('beerxmlv1/recipes.xml')).first
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should deserialize the base recipe data" do
|
12
|
+
@recipe.name.should == "Burton Ale"
|
13
|
+
@recipe.brewer.should == "Brad Smith"
|
14
|
+
@recipe.method.should == "All Grain"
|
15
|
+
@recipe.type.should == "Ale"
|
16
|
+
|
17
|
+
@recipe.recipe_volume.class.should == Unit
|
18
|
+
@recipe.recipe_volume.kind.should == :volume
|
19
|
+
@recipe.recipe_volume.scalar_in('gal').should be_within(0.01).of(5)
|
20
|
+
|
21
|
+
@recipe.boil_volume.class.should == Unit
|
22
|
+
@recipe.boil_volume.kind.should == :volume
|
23
|
+
@recipe.boil_volume.scalar_in('gal').should be_within(0.01).of(5.5)
|
24
|
+
|
25
|
+
@recipe.boil_time.class.should == Unit
|
26
|
+
@recipe.boil_time.kind.should == :time
|
27
|
+
@recipe.boil_time.should == "60 min".u
|
28
|
+
|
29
|
+
@recipe.recipe_efficiency.should == 72.0
|
30
|
+
|
31
|
+
@recipe.estimated_og.should == 1.056
|
32
|
+
@recipe.estimated_fg.should == 1.015
|
33
|
+
@recipe.estimated_color.should == 7.0
|
34
|
+
@recipe.estimated_ibu.should == 32.4
|
35
|
+
@recipe.estimated_abv.should == 5.3
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should deserialize the hop data" do
|
39
|
+
@recipe.hops.count.should == 4
|
40
|
+
h=@recipe.hops[1]
|
41
|
+
h.name.should == "Northern Brewer"
|
42
|
+
h.origin.should == "Germany"
|
43
|
+
h.alpha_acids.should == 7.5
|
44
|
+
h.amount.should === "0.50 oz".u
|
45
|
+
h.added_when.should == "Boil"
|
46
|
+
h.time.should === "60 min".u
|
47
|
+
h.description.should_not be_nil
|
48
|
+
h.type.should == "Both"
|
49
|
+
h.form.should == "Pellet"
|
50
|
+
h.beta_acids.should == 4.0
|
51
|
+
h.storageability.should == 35.0
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should deserialize the fermentable data" do
|
55
|
+
@recipe.fermentables.count.should == 3
|
56
|
+
f=@recipe.fermentables[0]
|
57
|
+
f.name.should == "Pale Malt (2 Row) UK"
|
58
|
+
f.origin.should == "United Kingdom"
|
59
|
+
f.type.should == "Grain"
|
60
|
+
f.description.should_not be_nil
|
61
|
+
f.amount.should === "8 lb".u
|
62
|
+
f.potential.should == 1.036
|
63
|
+
f.color.should == 2.5
|
64
|
+
f.moisture.should == 4.0
|
65
|
+
f.diastatic_power.should == 45.0
|
66
|
+
f.protein.should == 10.1
|
67
|
+
f.late_addition.should == false
|
68
|
+
f.recommend_mash.should == false
|
69
|
+
f.max_in_batch.should == 100.0
|
70
|
+
f.ibu_gal_per_lb.should == 0.0
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should deserialize the additive data" do
|
74
|
+
@recipe.additives.count.should == 2
|
75
|
+
a=@recipe.additives[0]
|
76
|
+
a.name.should == "Irish Moss"
|
77
|
+
a.type.should == "Fining"
|
78
|
+
a.description.should_not be_nil
|
79
|
+
a.added_when.should == "Boil"
|
80
|
+
a.amount.should == "0.25 tsp".u
|
81
|
+
a.time.should == "10 min".u
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should deserialize the yeast data" do
|
85
|
+
@recipe.yeasts.count.should == 1
|
86
|
+
y=@recipe.yeasts.first
|
87
|
+
y.name.should == "Burton Ale"
|
88
|
+
y.type.should == "Ale"
|
89
|
+
y.form.should == "Liquid"
|
90
|
+
y.supplier.should == "White Labs"
|
91
|
+
y.catalog.should == "WLP023"
|
92
|
+
y.description.should_not be_nil
|
93
|
+
y.best_for.should_not be_nil
|
94
|
+
y.min_temperature.should == "68 dF".u
|
95
|
+
y.max_temperature.should == "73 dF".u
|
96
|
+
y.amount.should == "35 ml".u
|
97
|
+
y.add_to_secondary?.should == false
|
98
|
+
y.flocculation.should == "Medium"
|
99
|
+
y.attenuation.should == 72.0
|
100
|
+
y.max_reuse.should == 5
|
101
|
+
y.times_cultured.should == 0
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should deserialize the mash schedule data" do
|
105
|
+
m=@recipe.mash_schedule
|
106
|
+
m.name.should == "Single Infusion, Full Body"
|
107
|
+
m.grain_temp.should == "72 dF".u
|
108
|
+
m.sparge_temp.should == "168 dF".u
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should deserialize the mash step data" do
|
112
|
+
m=@recipe.mash_schedule
|
113
|
+
m.mash_steps.count.should == 2
|
114
|
+
s=m.mash_steps[0]
|
115
|
+
# @TODO Fix this
|
116
|
+
s.index.should == 1
|
117
|
+
s.name.should == "Mash In"
|
118
|
+
s.type.should == "Infusion"
|
119
|
+
s.description.should_not be_nil
|
120
|
+
s.rest_time.should == "45 min".u
|
121
|
+
s.rest_temperature.should == "70 dC".u
|
122
|
+
s.infusion_volume.should == "11.25 qt".u
|
123
|
+
s.infusion_temperature.should == "170.5 dF".u
|
124
|
+
s.water_to_grain_ratio.should == 1.25
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should deserialize the fermentation step data" do
|
128
|
+
f=@recipe.fermentation_schedule
|
129
|
+
f.fermentation_steps.count.should == 3
|
130
|
+
s=f.fermentation_steps[0]
|
131
|
+
s.index.should == 1
|
132
|
+
s.name.should == "Primary"
|
133
|
+
s.time.should == "4 days".u
|
134
|
+
s.temperature.should == "68 dF".u
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should deserialize the water profile data" do
|
138
|
+
w=@recipe.water_profile
|
139
|
+
w.name.should == "Burton On Trent, UK"
|
140
|
+
w.description.should == "Distinctive pale ales strongly hopped. Very hard water accentuates the hops flavor.\nExample: Bass Ale"
|
141
|
+
w.calcium.should == 295.0
|
142
|
+
w.bicarbonate.should == 300.0
|
143
|
+
w.sulfates.should == 725.0
|
144
|
+
w.chloride.should == 25.0
|
145
|
+
w.sodium.should == 55.0
|
146
|
+
w.magnesium.should == 45.0
|
147
|
+
w.ph.should ==8.0
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should deserialize the style data" do
|
151
|
+
s=@recipe.style
|
152
|
+
s.name.should == "English Pale Ale"
|
153
|
+
s.category.should == "Bitter & English Pale Ale"
|
154
|
+
s.style_letter.should == "A"
|
155
|
+
s.style_guide.should == "BJCP 1999"
|
156
|
+
s.type.should == "Ale"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|