nrb-beerxml 0.0.2

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +33 -0
  4. data/lib/beerxml.rb +24 -0
  5. data/lib/beerxml/builder.rb +30 -0
  6. data/lib/beerxml/equipment.rb +38 -0
  7. data/lib/beerxml/fermentable.rb +33 -0
  8. data/lib/beerxml/hop.rb +38 -0
  9. data/lib/beerxml/inflector.rb +27 -0
  10. data/lib/beerxml/mash.rb +35 -0
  11. data/lib/beerxml/mash_step.rb +26 -0
  12. data/lib/beerxml/misc.rb +24 -0
  13. data/lib/beerxml/parser.rb +114 -0
  14. data/lib/beerxml/recipe.rb +109 -0
  15. data/lib/beerxml/record.rb +49 -0
  16. data/lib/beerxml/record_set.rb +39 -0
  17. data/lib/beerxml/record_validators.rb +8 -0
  18. data/lib/beerxml/record_validators/boolean_validator.rb +16 -0
  19. data/lib/beerxml/record_validators/percentage_validator.rb +38 -0
  20. data/lib/beerxml/style.rb +49 -0
  21. data/lib/beerxml/version.rb +6 -0
  22. data/lib/beerxml/water.rb +26 -0
  23. data/lib/beerxml/yeast.rb +38 -0
  24. data/spec/cases/beerxml/builder_spec.rb +107 -0
  25. data/spec/cases/beerxml/equipment_spec.rb +38 -0
  26. data/spec/cases/beerxml/fermentable_spec.rb +17 -0
  27. data/spec/cases/beerxml/hop_spec.rb +21 -0
  28. data/spec/cases/beerxml/mash_spec.rb +25 -0
  29. data/spec/cases/beerxml/mash_step_spec.rb +39 -0
  30. data/spec/cases/beerxml/misc_spec.rb +14 -0
  31. data/spec/cases/beerxml/parser_spec.rb +30 -0
  32. data/spec/cases/beerxml/recipe_spec.rb +98 -0
  33. data/spec/cases/beerxml/record_set_spec.rb +36 -0
  34. data/spec/cases/beerxml/record_spec.rb +15 -0
  35. data/spec/cases/beerxml/record_validators/boolean_validator_spec.rb +50 -0
  36. data/spec/cases/beerxml/record_validators/percentage_validator_spec.rb +98 -0
  37. data/spec/cases/beerxml/style_spec.rb +36 -0
  38. data/spec/cases/beerxml/version_spec.rb +13 -0
  39. data/spec/cases/beerxml/water_spec.rb +25 -0
  40. data/spec/cases/beerxml/yeast_spec.rb +20 -0
  41. data/spec/cases/beerxml_spec.rb +3 -0
  42. data/spec/fixtures/equipment.xml +28 -0
  43. data/spec/fixtures/recipes.xml +1412 -0
  44. data/spec/shared/active_model_lint.rb +21 -0
  45. data/spec/shared/record_typing.rb +19 -0
  46. data/spec/spec_helper.rb +8 -0
  47. metadata +210 -0
@@ -0,0 +1,33 @@
1
+ # NRB::Beerxml
2
+
3
+ [![Code Climate](https://codeclimate.com/github/NewRepublicBrewing/beerxml/badges/gpa.svg)](https://codeclimate.com/github/NewRepublicBrewing/beerxml)
4
+
5
+ A BeerXML parser
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'nrb-beerxml'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ % bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ % gem install nrb-beerxml
22
+
23
+ ## Usage
24
+
25
+
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/NewRepublicBrewing/nrb-beerxml/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some beers'`)
32
+ 4. Push to the branch (`git push origin my-new-beer`)
33
+ 5. Create a Pull Request
@@ -0,0 +1,24 @@
1
+ module NRB
2
+ module BeerXML
3
+
4
+ autoload :Builder, 'beerxml/builder'
5
+ autoload :Equipment, 'beerxml/equipment'
6
+ autoload :Fermentable, 'beerxml/fermentable'
7
+ autoload :Hop, 'beerxml/hop'
8
+ autoload :Inflector, 'beerxml/inflector'
9
+ autoload :Mash, 'beerxml/mash'
10
+ autoload :MashStep, 'beerxml/mash_step'
11
+ autoload :Misc, 'beerxml/misc'
12
+ autoload :Parser, 'beerxml/parser'
13
+ autoload :Recipe, 'beerxml/recipe'
14
+ autoload :Record, 'beerxml/record'
15
+ autoload :RecordSet, 'beerxml/record_set'
16
+ autoload :RecordValidators, 'beerxml/record_validators'
17
+ autoload :Style, 'beerxml/style'
18
+ autoload :Water, 'beerxml/water'
19
+ autoload :Yeast, 'beerxml/yeast'
20
+
21
+ end
22
+ end
23
+
24
+ defined?(BeerXML) || BeerXML = NRB::BeerXML
@@ -0,0 +1,30 @@
1
+ module NRB; module BeerXML
2
+ class Builder
3
+
4
+ include Inflector
5
+
6
+ RECORDS = %w( Equipment Fermentable Hop Mash MashStep Misc Recipe Style Water Yeast )
7
+
8
+ def self.records; RECORDS; end
9
+ def self.record_sets; records.map() { |type| "#{type}s" }; end
10
+
11
+
12
+ def build(type)
13
+ if record_set? type
14
+ RecordSet.new record_type: underscore(type.sub(/s$/,'')).to_sym
15
+ else
16
+ ::Module.nesting[1].const_get(type).new
17
+ end
18
+ rescue NameError
19
+ nil
20
+ end
21
+
22
+ private
23
+
24
+
25
+ def record_set?(type)
26
+ self.class.record_sets.include? type
27
+ end
28
+
29
+ end
30
+ end; end
@@ -0,0 +1,38 @@
1
+ require 'beerxml/record_validators/boolean_validator'
2
+ require 'beerxml/record_validators/percentage_validator'
3
+ module NRB
4
+ module BeerXML
5
+ class Equipment < Record
6
+
7
+ attr_accessor :batch_size # volume required
8
+ attr_accessor :boil_size # volume required
9
+ attr_accessor :boil_time # time
10
+ attr_accessor :calc_boil_volume # boolean
11
+ attr_accessor :evap_rate # percent
12
+ attr_accessor :hop_utilization # percent
13
+ attr_accessor :lauter_tun_deadspace # volume
14
+ attr_accessor :notes # string
15
+ attr_accessor :top_up_kettle # volume
16
+ attr_accessor :top_up_water # volume
17
+ attr_accessor :trub_chiller_loss # volume
18
+ attr_accessor :tun_specific_heat # float
19
+ attr_accessor :tun_volume # volume
20
+ attr_accessor :tun_weight # weight
21
+
22
+ validates :batch_size, numericality: { greater_than_or_equal_to: 0 }, presence: true
23
+ validates :boil_size, numericality: { greater_than_or_equal_to: 0 }, presence: true
24
+ validates :boil_time, numericality: { greater_than_or_equal_to: 0 }
25
+ validates :calc_boil_volume, boolean: true
26
+ validates :evap_rate, percentage: true
27
+ validates :hop_utilization, percentage: { give_110: true }
28
+ validates :lauter_tun_deadspace, numericality: { greater_than_or_equal_to: 0 }
29
+ validates :top_up_kettle, numericality: { greater_than_or_equal_to: 0 }
30
+ validates :top_up_water, numericality: { greater_than_or_equal_to: 0 }
31
+ validates :trub_chiller_loss, numericality: { greater_than_or_equal_to: 0 }
32
+ validates :tun_specific_heat, numericality: { greater_than_or_equal_to: 0 }
33
+ validates :tun_volume, numericality: { greater_than_or_equal_to: 0 }
34
+ validates :tun_weight, numericality: { greater_than_or_equal_to: 0 }
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ require 'beerxml/record_validators/boolean_validator' # hack so "validates percentage:" magic works
2
+ require 'beerxml/record_validators/percentage_validator' # hack so "validates percentage:" magic works
3
+
4
+ module NRB
5
+ module BeerXML
6
+ class Fermentable < Record
7
+
8
+ attr_accessor :add_after_boil # boolean
9
+ attr_accessor :amount # weight required
10
+ attr_accessor :coarse_fine_diff # percent
11
+ attr_accessor :color # float required
12
+ attr_accessor :diastatic_power # float
13
+ attr_accessor :ibu_gal_per_lb # float
14
+ attr_accessor :max_in_batch # percent
15
+ attr_accessor :moisture # percent
16
+ attr_accessor :notes # text
17
+ attr_accessor :origin # text
18
+ attr_accessor :recommend_mash # boolean
19
+ attr_accessor :supplier # text
20
+ attr_accessor :type # list required
21
+ attr_accessor :yield # percent required
22
+
23
+ validates :add_after_boil, boolean: true
24
+ validates :amount, presence: true
25
+ validates :color, presence: true
26
+ validates :type, inclusion: { in: [ "Adjunct", "Dry Extract", "Extract", "Grain", "Sugar" ] }, presence: true
27
+ validates :yield, presence: true
28
+
29
+ validates :diastatic_power, numericality: { greater_than_or_equal_to: 0 }
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'beerxml/record_validators/percentage_validator' # hack so "validates percentage:" magic works
2
+ module NRB
3
+ module BeerXML
4
+ class Hop < Record
5
+
6
+ attr_accessor :alpha # percentage required
7
+ attr_accessor :amount # weight required
8
+ attr_accessor :beta # percentage
9
+ attr_accessor :caryophyllene # percentage
10
+ attr_accessor :cohumulone # percentage
11
+ attr_accessor :form # list
12
+ attr_accessor :hsi # percentage
13
+ attr_accessor :myrcene # percentage
14
+ attr_accessor :notes # text
15
+ attr_accessor :origin # text
16
+ attr_accessor :substitutes # text
17
+ attr_accessor :time # time required
18
+ attr_accessor :type # list
19
+ attr_accessor :use # list required
20
+
21
+ validates :alpha, presence: true, percentage: true
22
+ validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
23
+ validates :beta, percentage: true
24
+ validates :caryophyllene, percentage: true
25
+ validates :cohumulone, percentage: true
26
+ validates :form, inclusion: { in: %w(Leaf Pellet Plug) }
27
+ validates :hsi, percentage: true
28
+ validates :myrcene, percentage: true
29
+ validates :time, presence: true,
30
+ numericality: { greater_than_or_equal_to: 0 }
31
+ validates :type, inclusion: { in: %w(Aroma Bittering Both) }
32
+
33
+ validates :use, presence: true,
34
+ inclusion: { in: [ "Aroma", "Boil", "Dry Hop", "First Wort", "Mash" ] }
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module NRB; module BeerXML
2
+ module Inflector
3
+
4
+ def camelize(term, uppercase_first_letter = true)
5
+ string = term.to_s
6
+ if uppercase_first_letter
7
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
8
+ else
9
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
10
+ end
11
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
12
+ string.gsub!('/', '::')
13
+ string
14
+ end
15
+
16
+
17
+ def underscore(camel_cased_word)
18
+ word = camel_cased_word.to_s.gsub('::', '/')
19
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
20
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
21
+ word.tr!("-", "_")
22
+ word.downcase!
23
+ word
24
+ end
25
+
26
+ end
27
+ end; end
@@ -0,0 +1,35 @@
1
+ require 'beerxml/record_validators/boolean_validator'
2
+ module NRB
3
+ module BeerXML
4
+ class Mash < Record
5
+
6
+ attr_accessor :equip_adjust # boolean
7
+ attr_accessor :grain_temp # temperature required
8
+ attr_accessor :mash_steps # record set required
9
+ attr_accessor :notes # string
10
+ attr_accessor :ph # float
11
+ attr_accessor :sparge_temp # temperature
12
+ attr_accessor :tun_specific_heat # float
13
+ attr_accessor :tun_temp # temperature
14
+ attr_accessor :tun_weight # weight
15
+
16
+ validates :grain_temp, numericality: true, presence: true
17
+ validates :mash_steps, presence: true
18
+ validates :ph, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 14 }
19
+ validates :sparge_temp, numericality: true
20
+ validates :tun_specific_heat, numericality: { greater_than_or_equal_to: 0 }
21
+ validates :tun_temp, numericality: true
22
+ validates :tun_weight, numericality: { greater_than_or_equal_to: 0 }
23
+
24
+
25
+ after_initialize :initialize_mash_steps
26
+
27
+ private
28
+
29
+ def initialize_mash_steps
30
+ self.mash_steps ||= RecordSet.new(record_type: :mash_step)
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ module NRB
2
+ module BeerXML
3
+ class MashStep < Record
4
+
5
+ attr_accessor :end_temp # temperature
6
+ attr_accessor :infuse_amount # volume conditionally required
7
+ attr_accessor :ramp_time # time
8
+ attr_accessor :step_temp # temperature required
9
+ attr_accessor :step_time # time required
10
+ attr_accessor :type # list required
11
+
12
+ validates :end_temp, numericality: true
13
+ validates :infuse_amount, presence: { if: Proc.new { |step| step.infuse_amount_required? } }, numericality: true
14
+ validates :ramp_time, numericality: { greater_than_or_equal_to: 0 }
15
+ validates :step_temp, numericality: true, presence: true
16
+ validates :step_time, numericality: { greater_than_or_equal_to: 0 }, presence: true
17
+ validates :type, inclusion: { in: %w(Decoction Infusion Temperature) },
18
+ presence: true
19
+
20
+ def infuse_amount_required?
21
+ type == "Infusion"
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'beerxml/record_validators/boolean_validator' # hack so "validates boolean:" magic works
2
+ module NRB
3
+ module BeerXML
4
+ class Misc < Record
5
+
6
+ attr_accessor :amount # float required
7
+ attr_accessor :amount_is_weight # boolean
8
+ attr_accessor :notes # string
9
+ attr_accessor :time # time required
10
+ attr_accessor :type # list required
11
+ attr_accessor :use # list required
12
+ attr_accessor :use_for # string
13
+
14
+ validates :amount, numericality: { greater_than_or_equal_to: 0 }, presence: true
15
+ validates :amount_is_weight, boolean: true
16
+ validates :time, numericality: { greater_than_or_equal_to: 0 },
17
+ presence: true
18
+ validates :type, inclusion: { in: ["Fining", "Flavor", "Herb", "Other", "Spice", "Water Agent"] }, presence: true
19
+ validates :use, inclusion: { in: %w(Boil Bottling Mash Primary Secondary) }, presence: true
20
+
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,114 @@
1
+ require 'nokogiri'
2
+
3
+ module NRB; module BeerXML
4
+ class Parser
5
+
6
+ include Inflector
7
+
8
+ attr_reader :builder, :reader
9
+
10
+ def initialize(builder: Builder.new, reader: Nokogiri::XML)
11
+ @builder = builder
12
+ @reader = reader
13
+ end
14
+
15
+
16
+ def parse(entry)
17
+ doc = case entry
18
+ when IO
19
+ parse_xml entry
20
+ when String
21
+ parse_path(file: entry)
22
+ else
23
+ raise ArgumentError "Don't know how to parse a #{entry.class}"
24
+ end
25
+ parse_node doc.root
26
+ end
27
+
28
+ private
29
+
30
+ def assign_attribute(object, node)
31
+ return unless object
32
+ return unless attribute_node?(node)
33
+ meth = guess_attribute_assignment_method node
34
+ if object.respond_to? meth
35
+ object.send meth, attribute_value(node)
36
+ end
37
+ end
38
+
39
+
40
+ def assign_child_to_parent(parent, child)
41
+ meth = guess_child_assignment_method parent, child
42
+ parent.respond_to?(meth) && parent.send(meth, child)
43
+ end
44
+
45
+
46
+ def attribute_node?(node)
47
+ node.children.size == 1 && node.children.first.text?
48
+ end
49
+
50
+
51
+ def attribute_value(node)
52
+ node.children.first.text
53
+ end
54
+
55
+
56
+ def guess_attribute_assignment_method(node)
57
+ node.name.downcase + '='
58
+ end
59
+
60
+
61
+ def guess_child_assignment_method(parent, child)
62
+ # Smelly, needs refactor
63
+ return :<< if parent.is_a? RecordSet
64
+ name = if child.is_a? RecordSet
65
+ "#{child.record_type.to_s}s"
66
+ else
67
+ child.class.name.split(/::/).last
68
+ end
69
+ underscore(name) + '='
70
+ end
71
+
72
+
73
+ def guess_class_name(string)
74
+ camelize string.downcase
75
+ end
76
+
77
+
78
+ def parse_node(node,parent=nil)
79
+ return unless node
80
+
81
+ if attribute_node?(node)
82
+ assign_attribute parent, node
83
+
84
+ else
85
+
86
+ obj = builder.build guess_class_name(node.name)
87
+
88
+ node.children.each do |child|
89
+ parse_node child, obj
90
+ end
91
+
92
+ assign_child_to_parent parent, obj
93
+
94
+ obj
95
+ end
96
+ end
97
+
98
+
99
+ def parse_path(file: nil)
100
+ f = File.open(file)
101
+ doc = parse_xml(f)
102
+ f.close
103
+ doc
104
+ end
105
+
106
+
107
+ def parse_xml(stream)
108
+ reader.parse(stream) do |config|
109
+ config.noblanks
110
+ end
111
+ end
112
+
113
+ end
114
+ end; end
@@ -0,0 +1,109 @@
1
+ require 'beerxml/record_validators/boolean_validator' # hack so "validates boolean:" magic works
2
+ require 'beerxml/record_validators/percentage_validator' # hack so "validates percentag:" magic works
3
+ module NRB
4
+ module BeerXML
5
+ class Recipe < Record
6
+
7
+ after_initialize :set_up_record_sets
8
+
9
+ attr_accessor :age # time
10
+ attr_accessor :age_temp # temperature
11
+ attr_accessor :asst_brewer # string
12
+ attr_accessor :batch_size # volume required
13
+ attr_accessor :boil_size # volume required
14
+ attr_accessor :boil_time # time required
15
+ attr_accessor :brewer # string required
16
+ attr_accessor :carbonation # float
17
+ attr_accessor :carbonation_temp # temperature
18
+ attr_accessor :date # string
19
+ attr_accessor :efficiency # percentage conditionally required
20
+ attr_reader :equipment # equipment record
21
+ attr_accessor :fermentables # fermentables record set required
22
+ attr_accessor :fermentation_stages # integer
23
+ attr_accessor :fg # specific gravity
24
+ attr_accessor :forced_carbonation # boolean
25
+ attr_accessor :hops # hops record set required
26
+ attr_accessor :keg_priming_factor # float
27
+ attr_accessor :mash # mash profile record required
28
+ attr_accessor :miscs # miscs record set required
29
+ attr_accessor :notes # string
30
+ attr_accessor :og # specific gravity
31
+ attr_accessor :primary_age # time
32
+ attr_accessor :primary_temp # temperature
33
+ attr_accessor :priming_sugar_equiv # float
34
+ attr_accessor :priming_sugar_name # string
35
+ attr_accessor :secondary_age # time
36
+ attr_accessor :secondary_temp # temperature
37
+ attr_reader :style # style record required
38
+ attr_accessor :taste_notes # string
39
+ attr_accessor :taste_rating # float
40
+ attr_accessor :tertiary_age # time
41
+ attr_accessor :tertiary_temp # temperature
42
+ attr_accessor :type # list required
43
+ attr_accessor :waters # waters record set required
44
+ attr_accessor :yeasts # yeasts record set required
45
+
46
+ validates :age, numericality: { greater_than_or_equal_to: 0 }
47
+ validates :age_temp, numericality: true
48
+ validates :batch_size, numericality: { greater_than_or_equal_to: 0 }, presence: true
49
+ validates :boil_size, numericality: { greater_than_or_equal_to: 0 }, presence: true
50
+ validates :boil_time, numericality: { greater_than_or_equal_to: 0 }, presence: true
51
+ validates :brewer, presence: true
52
+ validates :carbonation, numericality: { greater_than_or_equal_to: 0 }
53
+ validates :carbonation_temp, numericality: true
54
+ validates :equipment, presence: true
55
+ validates :efficiency, percentage: true, presence: { if: Proc.new { |recipe| recipe.efficiency_required? } }
56
+ validates :fermentables, presence: true
57
+ validates :fermentation_stages, numericality: { only_integer: true }
58
+ validates :fg, numericality: true
59
+ validates :forced_carbonation, boolean: true
60
+ validates :hops, presence: true
61
+ validates :keg_priming_factor, numericality: true
62
+ validates :mash, presence: true
63
+ validates :miscs, presence: true
64
+ validates :og, numericality: true
65
+ validates :primary_age, numericality: { greater_than_or_equal_to: 0 }
66
+ validates :primary_temp, numericality: true
67
+ validates :priming_sugar_equiv, numericality: true
68
+ validates :secondary_age, numericality: { greater_than_or_equal_to: 0 }
69
+ validates :secondary_temp, numericality: true
70
+ validates :style, presence: true
71
+ validates :taste_rating, numericality: true
72
+ validates :tertiary_age, numericality: { greater_than_or_equal_to: 0 }
73
+ validates :tertiary_temp, numericality: true
74
+ validates :type, inclusion: { in: [ "All Grain", "Extract", "Partial Mash" ] }, presence: true
75
+ validates :waters, presence: true
76
+ validates :yeasts, presence: true
77
+
78
+
79
+ def equipment=(equipment)
80
+ restricted_assign :equipment, equipment
81
+ end
82
+
83
+
84
+ def efficiency_required?
85
+ ["ALl Grain", "Partial Mash"].include?(type)
86
+ end
87
+
88
+
89
+ def set_up_record_sets
90
+ %i(fermentable hop misc water yeast).each do |record_type|
91
+ send "#{record_type}s=", RecordSet.new(record_type: record_type)
92
+ end
93
+ end
94
+
95
+
96
+ def style=(style)
97
+ restricted_assign :style, style
98
+ end
99
+
100
+ private
101
+
102
+ def restricted_assign(type, value)
103
+ raise "Can't assign a non-#{type} to #{type} (got a #{value.record_type}" unless value.nil? || value.send("#{type}?")
104
+ instance_variable_set "@#{type}", value
105
+ end
106
+
107
+ end
108
+ end
109
+ end