beerxml 0.0.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,7 +20,6 @@ Gem::Specification.new do |s|
20
20
  s.add_dependency "nokogiri", "~> 1.4"
21
21
  s.add_dependency "dm-core", "~> 1.0"
22
22
  s.add_dependency "dm-validations", "~> 1.0"
23
- s.add_dependency "dm-types", "~> 1.0"
24
23
 
25
24
  s.add_development_dependency "rspec", "~> 2.5"
26
25
  s.add_development_dependency "autotest-standalone"
@@ -1,5 +1,4 @@
1
1
  require 'dm-core'
2
- require 'dm-types'
3
2
  require 'dm-validations'
4
3
  require 'nokogiri'
5
4
 
@@ -1,6 +1,6 @@
1
1
  class Beerxml::Fermentable < Beerxml::Model
2
2
  property :name, String, :required => true
3
- property :type, Enum['Grain', 'Sugar', 'Extract', 'Dry Extract', 'Adjunct'], :required => true
3
+ property :type, String, :set => ['Grain', 'Sugar', 'Extract', 'Dry Extract', 'Adjunct'], :required => true
4
4
  property :amount, Weight, :required => true
5
5
  property :yield, Float, :required => true
6
6
  property :color, Float, :required => true
@@ -21,6 +21,10 @@ class Beerxml::Fermentable < Beerxml::Model
21
21
  property :id, Serial
22
22
  belongs_to :recipe, :required => false
23
23
 
24
+ def ppg=(ppg)
25
+ self.yield = Beerxml.round((ppg / 46.214) / 0.01, 2)
26
+ end
27
+
24
28
  def ppg
25
29
  # potential is (yield * 0.001 + 1)
26
30
  (self.yield * 0.01) * 46.214
@@ -31,4 +35,8 @@ class Beerxml::Fermentable < Beerxml::Model
31
35
  total *= (efficiency * 0.01) unless efficiency.nil? || type != 'Grain'
32
36
  total
33
37
  end
38
+
39
+ def total_color
40
+ amount.in_pounds.to_f * color
41
+ end
34
42
  end
@@ -2,12 +2,12 @@ class Beerxml::Hop < Beerxml::Model
2
2
  property :name, String, :required => true
3
3
  property :alpha, Float, :required => true
4
4
  property :amount, Weight, :required => true
5
- property :use, Enum['Boil', 'Dry Hop', 'Mash', 'First Wort', 'Aroma'], :required => true
5
+ property :use, String, :set => ['Boil', 'Dry Hop', 'Mash', 'First Wort', 'Aroma'], :required => true
6
6
  property :time, Time, :required => true
7
7
 
8
8
  property :notes, String, :length => 65535
9
- property :type, Enum[nil, 'Bittering', 'Aroma', 'Both']
10
- property :form, Enum[nil, 'Pellet', 'Plug', 'Leaf']
9
+ property :type, String, :set => ['Bittering', 'Aroma', 'Both']
10
+ property :form, String, :set => ['Pellet', 'Plug', 'Leaf']
11
11
  property :beta, Float
12
12
  property :hsi, Float
13
13
  property :origin, String, :length => 512
@@ -43,13 +43,10 @@ class Beerxml::Model
43
43
  read_xml_field(node, property.name.to_s)
44
44
  end
45
45
  # load any has-many relationships with other beerxml models
46
- relationships.each do |name, rel|
47
- child_model = rel.child_model
48
- next unless child_model.ancestors.include?(Beerxml::Model)
49
-
46
+ each_beerxml_relationship do |rel|
50
47
  # look for the plural element in the children of this node
51
48
  # e.g., for Hop, see if there's any HOPS element.
52
- (node>child_model.beerxml_plural_name).each do |child_wrapper_node|
49
+ (node>rel.child_model.beerxml_plural_name).each do |child_wrapper_node|
53
50
  collection = Beerxml::Model.collection_from_xml(child_wrapper_node)
54
51
  self.send(rel.name).concat(collection)
55
52
  end
@@ -83,9 +80,16 @@ class Beerxml::Model
83
80
  attributes.each do |attr, val|
84
81
  next if attr.to_s.match(/id\z/) || val.nil?
85
82
  x = Nokogiri::XML::Node.new(self.class.xml_attr_name(attr), parent)
86
- x.content = val # TODO: data types
83
+ x.content = self.class.properties[attr].dump(val)
87
84
  node.add_child(x)
88
85
  end
86
+ each_beerxml_relationship do |rel|
87
+ objs = self.send(rel.name)
88
+ next if objs.empty?
89
+ sub_node = Nokogiri::XML::Node.new(rel.child_model.beerxml_plural_name, node)
90
+ node.add_child(sub_node)
91
+ objs.each { |o| o.to_beerxml(sub_node) }
92
+ end
89
93
  parent.add_child(node)
90
94
  parent
91
95
  end
@@ -97,7 +101,18 @@ class Beerxml::Model
97
101
  def self.xml_attr_name(name)
98
102
  name.to_s.upcase
99
103
  end
104
+
105
+ def beerxml_relationships
106
+ []
107
+ end
108
+
109
+ def each_beerxml_relationship
110
+ relationships.each do |name, rel|
111
+ next unless beerxml_relationships.include?(rel.name)
112
+ yield rel
113
+ end
114
+ end
100
115
  end
101
116
 
102
- %w(hop recipe fermentable).
117
+ %w(hop recipe fermentable yeast).
103
118
  each { |f| require "beerxml/#{f}" }
@@ -8,10 +8,14 @@ module Beerxml::Properties
8
8
  true
9
9
  end
10
10
 
11
+ def primitive?(value)
12
+ value.is_a?(Beerxml::Unit) && value.type == unit_type
13
+ end
14
+
11
15
  def load(value)
12
16
  return if value.nil?
13
17
  if value.is_a?(Beerxml::Unit)
14
- raise(ArgumentError, 'Weight required') unless value.type == unit_type
18
+ raise(ArgumentError, "#{value.inspect} is not a #{unit_type}") unless value.type == unit_type
15
19
  value
16
20
  else
17
21
  U(value, base_unit)
@@ -2,7 +2,7 @@ class Beerxml::Recipe < Beerxml::Model
2
2
  include DataMapper::Resource
3
3
 
4
4
  property :name, String, :required => true
5
- property :type, Enum['Extract', 'Partial Mash', 'All Grain'], :required => true
5
+ property :type, String, :set => ['Extract', 'Partial Mash', 'All Grain'], :required => true
6
6
  # has 1, :style, :required => true
7
7
  property :brewer, String, :required => true
8
8
  property :batch_size, Volume, :required => true
@@ -14,9 +14,13 @@ class Beerxml::Recipe < Beerxml::Model
14
14
  has n, :hops
15
15
  has n, :fermentables
16
16
  # has n, :miscs
17
- # has n, :yeasts
17
+ has n, :yeasts
18
18
  # has n, :waters
19
19
 
20
+ def beerxml_relationships
21
+ [:hops, :fermentables, :miscs, :yeasts, :waters]
22
+ end
23
+
20
24
  property :asst_brewer, String
21
25
  property :notes, String, :length => 65535
22
26
  property :taste_notes, String, :length => 65535
@@ -47,7 +51,7 @@ class Beerxml::Recipe < Beerxml::Model
47
51
  property :id, Serial
48
52
 
49
53
  def tinseth
50
- Beerxml.round(hops.select { |h| h.use == 'Boil' }.inject(0) { |v, h| v + h.tinseth(og, batch_size) }, 1)
54
+ Beerxml.round(hops.select { |h| h.use == 'Boil' }.inject(0) { |v, h| v + h.tinseth(calculate_og, batch_size) }, 1)
51
55
  end
52
56
  alias_method :ibus, :tinseth
53
57
 
@@ -56,4 +60,14 @@ class Beerxml::Recipe < Beerxml::Model
56
60
  og = 1 + ((total_ppg / batch_size.in_gallons.to_f) * 0.001)
57
61
  Beerxml.round(og, 3)
58
62
  end
63
+
64
+ def calculate_fg
65
+ og = calculate_og
66
+ Beerxml.round(og - ((og - 1) * yeasts.first.attenuation * 0.01), 3)
67
+ end
68
+
69
+ def color
70
+ color = fermentables.inject(0) { |v, f| v + f.total_color }
71
+ Beerxml.round(1.4922 * ((color / batch_size.in_gallons.to_f) ** 0.6859), 1)
72
+ end
59
73
  end
@@ -96,25 +96,10 @@ class Unit
96
96
  end
97
97
  end
98
98
 
99
- # todo: remove this, to_beerxml is using it (and incorrectly at that)
100
- def to_s
101
- to_f.to_s
102
- end
103
-
104
99
  def inspect
105
100
  "[#{to_f} #{unit}]"
106
101
  end
107
102
 
108
- def *(rhs)
109
- if rhs.is_a?(Numeric)
110
- ret = Unit.new(self.to_f * rhs, self.unit)
111
- elsif rhs.is_a?(Unit) && rhs.type == type
112
- ret = Unit.new(self.to_f * rhs.to(self.unit).to_f, self.unit)
113
- else
114
- super
115
- end
116
- end
117
-
118
103
  # Assumes earth gravity ;)
119
104
  module Weight
120
105
  def self.included(k)
@@ -1,3 +1,3 @@
1
1
  module Beerxml
2
- VERSION = "0.0.2"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,26 @@
1
+ class Beerxml::Yeast < Beerxml::Model
2
+ include DataMapper::Resource
3
+
4
+ property :name, String, :required => true
5
+ property :type, String, :set => ['Ale', 'Lager', 'Wheat', 'Wine', 'Champagne'], :required => true
6
+ property :form, String, :set => ['Liquid', 'Dry', 'Slant', 'Culture'], :required => true
7
+ # TODO: sheesh... this can be a Weight instead, if amount_is_weight
8
+ property :amount, Volume, :required => true
9
+
10
+ property :amount_is_weight, Boolean
11
+ property :laboratory, String
12
+ property :product_id, String
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 :notes, String, :length => 65535
18
+ property :best_for, String
19
+ property :times_cultured, Integer
20
+ property :max_reuse, Integer
21
+ property :add_to_secondary, Boolean
22
+
23
+ # these are not used in the xml
24
+ property :id, Serial
25
+ belongs_to :recipe, :required => false
26
+ end
@@ -0,0 +1,37 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ describe "dumping" do
4
+ describe "sanity checks" do
5
+ it "should dump hops" do
6
+ model = Beerxml::Hop.new({
7
+ :name => 'Northern Brewer',
8
+ :origin => 'Germany',
9
+ :alpha => 8.5,
10
+ :amount => 0.0,
11
+ :use => 'Boil',
12
+ :time => 0.0,
13
+ :notes => %{Also called Hallertauer Northern Brewers
14
+ Use for: Bittering and finishing both ales and lagers of all kinds
15
+ Aroma: Fine, dry, clean bittering hop. Unique flavor.
16
+ Substitute: Hallertauer Mittelfrueh, Hallertauer
17
+ Examples: Anchor Steam, Old Peculiar, },
18
+ :type => 'Both',
19
+ :form => 'Pellet',
20
+ :beta => 4.0,
21
+ :hsi => 35.0,
22
+ })
23
+ model.should be_valid
24
+ xml = model.to_beerxml.root
25
+ (xml>'NAME').text.should == 'Northern Brewer'
26
+ (xml>'ORIGIN').text.should == 'Germany'
27
+ (xml>'ALPHA').text.should == '8.5'
28
+ (xml>'AMOUNT').text.should == '0.0'
29
+ (xml>'USE').text.should == 'Boil'
30
+ (xml>'TIME').text.should == '0.0'
31
+ (xml>'TYPE').text.should == 'Both'
32
+ (xml>'FORM').text.should == 'Pellet'
33
+ (xml>'BETA').text.should == '4.0'
34
+ (xml>'HSI').text.should == '35.0'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,106 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ include Beerxml
4
+
5
+ describe "Centennial Blonde" do
6
+ before(:each) do
7
+ # http://www.homebrewtalk.com/f66/centennial-blonde-simple-4-all-grain-5-10-gall-42841/
8
+ @recipe = Recipe.new(:name => 'Centennial Blonde',
9
+ :type => 'All Grain',
10
+ :brewer => 'BierMuncher',
11
+ :batch_size => 5.5.gallons,
12
+ :boil_size => 6.75.gallons,
13
+ :boil_time => 60.minutes,
14
+ :efficiency => 70.0)
15
+ @recipe.hops << Hop.new(:name => 'Centennial',
16
+ :alpha => 9.5,
17
+ :amount => 0.25.oz,
18
+ :use => 'Boil',
19
+ :time => 55)
20
+ @recipe.hops << Hop.new(:name => 'Centennial',
21
+ :alpha => 9.5,
22
+ :amount => 0.25.oz,
23
+ :use => 'Boil',
24
+ :time => 35)
25
+ @recipe.hops << Hop.new(:name => 'Cascade',
26
+ :alpha => 7.8,
27
+ :amount => 0.25.oz,
28
+ :use => 'Boil',
29
+ :time => 20)
30
+ @recipe.hops << Hop.new(:name => 'Cascade',
31
+ :alpha => 7.8,
32
+ :amount => 0.25.oz,
33
+ :use => 'Boil',
34
+ :time => 5)
35
+ @recipe.fermentables << Fermentable.new(:name => 'Pale Malt (2 Row)',
36
+ :type => 'Grain',
37
+ :amount => 7.pounds,
38
+ :ppg => 37,
39
+ :color => 2)
40
+ @recipe.fermentables << Fermentable.new(:name => 'Cara-Pils',
41
+ :type => 'Grain',
42
+ :amount => 0.75.lb,
43
+ :ppg => 33,
44
+ :color => 2)
45
+ @recipe.fermentables << Fermentable.new(:name => 'Crystal 10L',
46
+ :type => 'Grain',
47
+ :amount => 0.5.pounds,
48
+ :ppg => 35,
49
+ :color => 10)
50
+ @recipe.fermentables << Fermentable.new(:name => 'Vienna',
51
+ :type => 'Grain',
52
+ :amount => U(0.5, 'lb'),
53
+ :ppg => 35,
54
+ :color => 4)
55
+ @recipe.yeasts << Yeast.new(:name => 'Nottingham',
56
+ :type => 'Ale',
57
+ :form => 'Dry',
58
+ :amount => 0.2.liters,
59
+ :attenuation => 75)
60
+ @recipe.should be_valid
61
+ end
62
+
63
+ it "should calculate basic stats" do
64
+ @recipe.ibus.round.should == 20
65
+ @recipe.calculate_og.should == 1.041
66
+ @recipe.calculate_fg.should == 1.010
67
+ @recipe.color == 3.9
68
+ end
69
+
70
+ it "should survive the serialization round trip" do
71
+ xml = Nokogiri::XML(@recipe.to_xml)
72
+ r2 = Recipe.new.from_xml(xml.root)
73
+ r2.should be_valid
74
+ r2.attributes.should == @recipe.attributes
75
+ r2.each_beerxml_relationship do |rel|
76
+ r2.send(rel.name).map(&:attributes).should == @recipe.send(rel.name).map(&:attributes)
77
+ end
78
+ end
79
+
80
+ it "should calculate basic stats for the extract version" do
81
+ @extract = Recipe.new(:name => 'Centennial Blonde',
82
+ :type => 'Extract',
83
+ :brewer => 'BierMuncher',
84
+ :batch_size => U(5.5, 'gallons'),
85
+ :boil_size => U(6.75, 'gallons'),
86
+ :boil_time => U(60, 'minutes'),
87
+ :efficiency => 70.0)
88
+ @extract.hops.concat @recipe.hops
89
+ @extract.yeasts.concat @recipe.yeasts
90
+ @extract.fermentables << Fermentable.new(:name => 'Extra Light DME',
91
+ :type => 'Dry Extract',
92
+ :amount => U(5, 'lb'),
93
+ :ppg => 43,
94
+ :color => 3)
95
+ @extract.fermentables << Fermentable.new(:name => 'Cara-Pils',
96
+ :type => 'Grain',
97
+ :amount => U(1, 'lb'),
98
+ :ppg => 33,
99
+ :color => 2)
100
+ @extract.should be_valid
101
+ @extract.ibus.round.should == 20
102
+ @extract.calculate_og.should == 1.043
103
+ @extract.calculate_fg.should == 1.011
104
+ @extract.color == 3.2
105
+ end
106
+ end
@@ -1,6 +1,6 @@
1
1
  require "#{File.dirname(__FILE__)}/spec_helper"
2
2
 
3
- describe "beerxml.com examples" do
3
+ describe "parsing" do
4
4
  it "should parse the first recipe and its hops" do
5
5
  recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
6
6
 
@@ -101,6 +101,26 @@ Examples: Anchor Steam, Old Peculiar, },
101
101
  :hsi => 35.0,
102
102
  })
103
103
  end
104
+ it "should parse yeasts" do
105
+ check_parse(Beerxml::Yeast, "yeast", 3, {
106
+ :name => 'European Ale',
107
+ :type => 'Ale',
108
+ :form => 'Liquid',
109
+ :amount => 0.035,
110
+ :amount_is_weight => false,
111
+ :laboratory => 'White Labs',
112
+ :product_id => 'WLP011',
113
+ :min_temperature => 18.3,
114
+ :max_temperature => 21.1,
115
+ :flocculation => 'Medium',
116
+ :attenuation => 67.5,
117
+ :notes => "Malty, Northern European ale yeast. Low ester production, low sulfer, gives a clean profile. Low attenuation contributes to malty taste.",
118
+ :best_for => "Alt, Kolsch, malty English Ales, Fruit beers",
119
+ :max_reuse => 5,
120
+ :times_cultured => 0,
121
+ :add_to_secondary => false,
122
+ })
123
+ end
104
124
  it "should parse recipes" do
105
125
  recipe = check_parse(Beerxml::Recipe, "recipes", 3, {
106
126
  :name => 'Dry Stout',
@@ -4,7 +4,7 @@ describe Beerxml::Recipe do
4
4
  it "should calculate IBUs using the tinseth method" do
5
5
  recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
6
6
  recipe.should be_valid
7
- recipe.ibus.round.should == 32
7
+ recipe.ibus.round.should == 31
8
8
  end
9
9
 
10
10
  it "should calculate the OG for an all grain batch" do
@@ -12,4 +12,10 @@ describe Beerxml::Recipe do
12
12
  recipe.should be_valid
13
13
  recipe.calculate_og.should == 1.056
14
14
  end
15
+
16
+ it "should calculate the FG for an all grain batch" do
17
+ recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
18
+ recipe.should be_valid
19
+ recipe.calculate_fg.should == 1.016
20
+ end
15
21
  end
@@ -2,6 +2,8 @@ require 'beerxml'
2
2
 
3
3
  require 'rspec'
4
4
 
5
+ Beerxml::Unit.apply_to_numeric!
6
+
5
7
  RSpec.configure do |c|
6
8
  def filename(example)
7
9
  "examples/beerxml.com/#{example}.xml"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beerxml
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 3
8
9
  - 0
9
- - 2
10
- version: 0.0.2
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Brian Palmer
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-07 00:00:00 -07:00
18
+ date: 2011-03-08 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -63,25 +63,10 @@ dependencies:
63
63
  version: "1.0"
64
64
  type: :runtime
65
65
  version_requirements: *id003
66
- - !ruby/object:Gem::Dependency
67
- name: dm-types
68
- prerelease: false
69
- requirement: &id004 !ruby/object:Gem::Requirement
70
- none: false
71
- requirements:
72
- - - ~>
73
- - !ruby/object:Gem::Version
74
- hash: 15
75
- segments:
76
- - 1
77
- - 0
78
- version: "1.0"
79
- type: :runtime
80
- version_requirements: *id004
81
66
  - !ruby/object:Gem::Dependency
82
67
  name: rspec
83
68
  prerelease: false
84
- requirement: &id005 !ruby/object:Gem::Requirement
69
+ requirement: &id004 !ruby/object:Gem::Requirement
85
70
  none: false
86
71
  requirements:
87
72
  - - ~>
@@ -92,11 +77,11 @@ dependencies:
92
77
  - 5
93
78
  version: "2.5"
94
79
  type: :development
95
- version_requirements: *id005
80
+ version_requirements: *id004
96
81
  - !ruby/object:Gem::Dependency
97
82
  name: autotest-standalone
98
83
  prerelease: false
99
- requirement: &id006 !ruby/object:Gem::Requirement
84
+ requirement: &id005 !ruby/object:Gem::Requirement
100
85
  none: false
101
86
  requirements:
102
87
  - - ">="
@@ -106,11 +91,11 @@ dependencies:
106
91
  - 0
107
92
  version: "0"
108
93
  type: :development
109
- version_requirements: *id006
94
+ version_requirements: *id005
110
95
  - !ruby/object:Gem::Dependency
111
96
  name: autotest-growl
112
97
  prerelease: false
113
- requirement: &id007 !ruby/object:Gem::Requirement
98
+ requirement: &id006 !ruby/object:Gem::Requirement
114
99
  none: false
115
100
  requirements:
116
101
  - - ">="
@@ -120,11 +105,11 @@ dependencies:
120
105
  - 0
121
106
  version: "0"
122
107
  type: :development
123
- version_requirements: *id007
108
+ version_requirements: *id006
124
109
  - !ruby/object:Gem::Dependency
125
110
  name: rcov
126
111
  prerelease: false
127
- requirement: &id008 !ruby/object:Gem::Requirement
112
+ requirement: &id007 !ruby/object:Gem::Requirement
128
113
  none: false
129
114
  requirements:
130
115
  - - ">="
@@ -134,11 +119,11 @@ dependencies:
134
119
  - 0
135
120
  version: "0"
136
121
  type: :development
137
- version_requirements: *id008
122
+ version_requirements: *id007
138
123
  - !ruby/object:Gem::Dependency
139
124
  name: yard
140
125
  prerelease: false
141
- requirement: &id009 !ruby/object:Gem::Requirement
126
+ requirement: &id008 !ruby/object:Gem::Requirement
142
127
  none: false
143
128
  requirements:
144
129
  - - ">="
@@ -148,7 +133,7 @@ dependencies:
148
133
  - 0
149
134
  version: "0"
150
135
  type: :development
151
- version_requirements: *id009
136
+ version_requirements: *id008
152
137
  description: |-
153
138
  Library for parsing and generating beerxml (http://www.beerxml.com/).
154
139
  More than that, this library also contains various methods for doing calculations on beer ingredients and recipes, among other helpers.
@@ -185,6 +170,9 @@ files:
185
170
  - lib/beerxml/recipe.rb
186
171
  - lib/beerxml/unit.rb
187
172
  - lib/beerxml/version.rb
173
+ - lib/beerxml/yeast.rb
174
+ - spec/dumping_spec.rb
175
+ - spec/full_stack_spec.rb
188
176
  - spec/hops_spec.rb
189
177
  - spec/parsing_spec.rb
190
178
  - spec/recipes_spec.rb
@@ -225,6 +213,8 @@ signing_key:
225
213
  specification_version: 3
226
214
  summary: Library for parsing and generating beerxml (http://www.beerxml.com/)
227
215
  test_files:
216
+ - spec/dumping_spec.rb
217
+ - spec/full_stack_spec.rb
228
218
  - spec/hops_spec.rb
229
219
  - spec/parsing_spec.rb
230
220
  - spec/recipes_spec.rb