brewscribe 0.0.1
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.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +40 -0
- data/Rakefile +11 -0
- data/brewscribe.gemspec +21 -0
- data/lib/brewscribe.rb +11 -0
- data/lib/brewscribe/conversion.rb +22 -0
- data/lib/brewscribe/grain.rb +40 -0
- data/lib/brewscribe/hops.rb +34 -0
- data/lib/brewscribe/ingredient_list.rb +58 -0
- data/lib/brewscribe/recipe.rb +76 -0
- data/lib/brewscribe/version.rb +3 -0
- data/lib/brewscribe/yeast.rb +44 -0
- data/spec/brewscribe_spec.rb +24 -0
- data/spec/conversion_spec.rb +37 -0
- data/spec/grain_spec.rb +54 -0
- data/spec/hops_spec.rb +52 -0
- data/spec/ingredient_list_spec.rb +51 -0
- data/spec/recipe_spec.rb +43 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/recipe.bsmx +502 -0
- data/spec/yeast_spec.rb +49 -0
- metadata +129 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
guard :rspec do
|
4
|
+
watch(%r{^spec/.+_spec\.rb$})
|
5
|
+
watch(%r{^lib/brewscribe/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
6
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec/" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec/" }
|
8
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Andrew Nordman
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Brewscribe
|
2
|
+
|
3
|
+
Brewscribe is a Beersmith2 (.bsmx) file parser.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
`gem install brewscribe` or `gem brewscribe` in your Gemfile.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
To start, you can import your .bsmx file with `Brewscribe.import(file)` where file
|
12
|
+
is any object that responds to `#read`. This will return an array of Brewscribe::Recipe
|
13
|
+
objects. You now have a parsed version of the recipe files.
|
14
|
+
|
15
|
+
By default, Brewscribe will set a text property for each attribute of the recipe, and
|
16
|
+
if it has a parser object it will attempt to further parse the data.
|
17
|
+
|
18
|
+
An example of this is found in `Brewscribe::IngredientList`:
|
19
|
+
|
20
|
+
```
|
21
|
+
recipe = Brewscribe.import File.read './spec/support/recipe.bsmx'
|
22
|
+
recipe.ingredients.class # => Brewscribe::IngredientList
|
23
|
+
recipe.ingredients.grains.class # => Array
|
24
|
+
recipe.ingredients.grains.first.class # => Brewscribe::Grain
|
25
|
+
recipe.ingredients.grains.first.name # => "Pale Malt (2 Row) US"
|
26
|
+
```
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
I <3 Contributions.
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
37
|
+
|
38
|
+
## Author
|
39
|
+
|
40
|
+
Created by [Andrew Nordman](https://github.com/cadwallion/).
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
6
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
7
|
+
spec.rspec_opts = ['--backtrace']
|
8
|
+
# spec.ruby_opts = ['-w']
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
data/brewscribe.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/brewscribe/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Andrew Nordman"]
|
6
|
+
gem.email = ["cadwallion@gmail.com"]
|
7
|
+
gem.summary = %q{A Beersmith (.bsmx) file parser}
|
8
|
+
gem.homepage = ""
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($\)
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "brewscribe"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Brewscribe::VERSION
|
16
|
+
|
17
|
+
gem.add_development_dependency 'rspec'
|
18
|
+
gem.add_development_dependency 'guard'
|
19
|
+
gem.add_development_dependency 'guard-rspec'
|
20
|
+
gem.add_dependency 'nokogiri'
|
21
|
+
end
|
data/lib/brewscribe.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Brewscribe
|
2
|
+
module Conversion
|
3
|
+
BOOLEAN_CONV = ->(k) { k == '1' }
|
4
|
+
FLOAT_CONV = ->(k) { k.to_f }
|
5
|
+
PERCENT_CONV = ->(k) { k.to_f * 0.001 }
|
6
|
+
INT_CONV = ->(k) { k.to_i }
|
7
|
+
DATE_CONV = ->(k) { Date.parse k }
|
8
|
+
|
9
|
+
|
10
|
+
def data_to_properties data
|
11
|
+
data.each_key do |key|
|
12
|
+
if self.class.const_get(:KEY_CONVERSION).has_key? key
|
13
|
+
value = self.class.const_get(:KEY_CONVERSION)[key].call(data[key])
|
14
|
+
else
|
15
|
+
value = data[key]
|
16
|
+
end
|
17
|
+
|
18
|
+
self.instance_variable_set "@#{key}".to_sym, value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'brewscribe/conversion'
|
2
|
+
|
3
|
+
module Brewscribe
|
4
|
+
class Grain
|
5
|
+
attr_reader :name, :origin, :amount, :color, :yield, :percent, :price,
|
6
|
+
:recommend_mash, :in_recipe, :type, :add_after_boil, :notes, :boil_time,
|
7
|
+
:max_in_batch, :ibu_gal_per_lb, :protein, :diastatic_power, :late_extract,
|
8
|
+
:convert_grain, :moisture, :coarse_fine_diff, :convert_grain, :supplier
|
9
|
+
|
10
|
+
include Brewscribe::Conversion
|
11
|
+
|
12
|
+
TYPES = ['Grain', 'Extract Sugar', 'Adjunct', 'Dry Extract']
|
13
|
+
|
14
|
+
KEY_CONVERSION = {
|
15
|
+
amount: FLOAT_CONV,
|
16
|
+
color: FLOAT_CONV,
|
17
|
+
yield: PERCENT_CONV,
|
18
|
+
price: FLOAT_CONV,
|
19
|
+
boil_time: ->(k) { k.to_i },
|
20
|
+
percent: PERCENT_CONV,
|
21
|
+
max_in_batch: PERCENT_CONV,
|
22
|
+
add_after_boil: BOOLEAN_CONV,
|
23
|
+
recommend_mash: BOOLEAN_CONV,
|
24
|
+
in_recipe: BOOLEAN_CONV,
|
25
|
+
type: ->(k) { TYPES[k.to_i] },
|
26
|
+
ibu_gal_per_lb: FLOAT_CONV,
|
27
|
+
protein: PERCENT_CONV,
|
28
|
+
diastatic_power: PERCENT_CONV,
|
29
|
+
late_extract: FLOAT_CONV,
|
30
|
+
moisture: PERCENT_CONV,
|
31
|
+
coarse_fine_diff: PERCENT_CONV
|
32
|
+
}
|
33
|
+
|
34
|
+
def initialize grain_data
|
35
|
+
@original_data = grain_data
|
36
|
+
|
37
|
+
data_to_properties grain_data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Brewscribe
|
2
|
+
class Hops
|
3
|
+
attr_reader :name, :origin, :alpha, :beta, :notes, :boil_time, :percent,
|
4
|
+
:amount, :hsi, :dry_hop_time, :ibu_contrib, :use, :in_recipe, :price,
|
5
|
+
:type, :form
|
6
|
+
|
7
|
+
include Brewscribe::Conversion
|
8
|
+
|
9
|
+
KEY_CONVERSION = {
|
10
|
+
alpha: FLOAT_CONV,
|
11
|
+
beta: FLOAT_CONV,
|
12
|
+
boil_time: ->(k) { k.to_i },
|
13
|
+
percent: PERCENT_CONV,
|
14
|
+
amount: FLOAT_CONV,
|
15
|
+
hsi: FLOAT_CONV,
|
16
|
+
dry_hop_time: ->(k) { k.to_i },
|
17
|
+
ibu_contrib: PERCENT_CONV,
|
18
|
+
use: ->(k) { USES[k.to_i] },
|
19
|
+
type: ->(k) { TYPES[k.to_i] },
|
20
|
+
form: ->(k) { FORMS[k.to_i] },
|
21
|
+
in_recipe: BOOLEAN_CONV,
|
22
|
+
price: FLOAT_CONV
|
23
|
+
}
|
24
|
+
|
25
|
+
TYPES = ['Bittering', 'Aroma', 'Both']
|
26
|
+
FORMS = ['Pellet', 'Plug', 'Leaf']
|
27
|
+
USES = ['Boil', 'Dry Hop', 'Mash', 'First Wort', 'Aroma']
|
28
|
+
|
29
|
+
def initialize data
|
30
|
+
@original_data = data
|
31
|
+
data_to_properties data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'brewscribe/grain'
|
2
|
+
require 'brewscribe/hops'
|
3
|
+
require 'brewscribe/yeast'
|
4
|
+
|
5
|
+
module Brewscribe
|
6
|
+
class IngredientList
|
7
|
+
attr_reader :grains, :hops, :yeasts
|
8
|
+
def self.from_data data
|
9
|
+
list = new
|
10
|
+
|
11
|
+
case data[:grain]
|
12
|
+
when Array
|
13
|
+
data[:grain].each do |grain|
|
14
|
+
list.add_grain grain
|
15
|
+
end
|
16
|
+
when Hash
|
17
|
+
list.add_graind data[:grain]
|
18
|
+
end
|
19
|
+
|
20
|
+
case data[:hops]
|
21
|
+
when Array
|
22
|
+
data[:hops].each do |hops|
|
23
|
+
list.add_hops hops
|
24
|
+
end
|
25
|
+
when Hash
|
26
|
+
list.add_hops data[:hops]
|
27
|
+
end
|
28
|
+
|
29
|
+
case data[:yeast]
|
30
|
+
when Array
|
31
|
+
data[:yeast].each do |yeast|
|
32
|
+
list.add_yeast yeast
|
33
|
+
end
|
34
|
+
when Hash
|
35
|
+
list.add_yeast data[:yeast]
|
36
|
+
end
|
37
|
+
list
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@grains = []
|
42
|
+
@hops = []
|
43
|
+
@yeasts = []
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_grain grain_data
|
47
|
+
@grains << Grain.new(grain_data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_hops hop_data
|
51
|
+
@hops << Hops.new(hop_data)
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_yeast yeast_data
|
55
|
+
@yeasts << Yeast.new(yeast_data)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Brewscribe
|
4
|
+
class Recipe
|
5
|
+
attr_reader :raw_data, :hash
|
6
|
+
|
7
|
+
def initialize raw_data
|
8
|
+
@raw_data = raw_data
|
9
|
+
|
10
|
+
parse_raw_data
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_raw_data
|
14
|
+
@xml = Nokogiri::XML(@raw_data).xpath('/Selections/Data/Recipe')
|
15
|
+
@hash = xml_node_to_hash(@xml.first)
|
16
|
+
|
17
|
+
create_recipe_accessors
|
18
|
+
parse_ingredients
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_recipe_accessors
|
22
|
+
@hash.keys.each do |key|
|
23
|
+
self.class.module_eval do
|
24
|
+
attr_accessor key
|
25
|
+
end
|
26
|
+
|
27
|
+
self.send "#{key}=", @hash[key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def clean_key key
|
32
|
+
extracted = key.to_s.match(/(F_(\w{1,2}_)?)?(_MOD_|.+)/)[3]
|
33
|
+
if extracted == '_MOD_'
|
34
|
+
return 'last_modified'
|
35
|
+
else
|
36
|
+
extracted.downcase
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_ingredients
|
41
|
+
self.ingredients = IngredientList.from_data(self.ingredients[:data])
|
42
|
+
end
|
43
|
+
|
44
|
+
def xml_node_to_hash node
|
45
|
+
if node.element?
|
46
|
+
if node.children.size > 0
|
47
|
+
result_hash = {}
|
48
|
+
|
49
|
+
node.children.each do |child|
|
50
|
+
result = xml_node_to_hash child
|
51
|
+
property = clean_key child.name
|
52
|
+
key = property.to_sym
|
53
|
+
|
54
|
+
if child.name == 'text'
|
55
|
+
return result if !child.next && !child.previous
|
56
|
+
elsif result_hash[key]
|
57
|
+
if result_hash[key].is_a? Array
|
58
|
+
result_hash[key] << result
|
59
|
+
else
|
60
|
+
result_hash[key] = [result_hash[key]] << result
|
61
|
+
end
|
62
|
+
else
|
63
|
+
result_hash[key] = result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return result_hash
|
68
|
+
else
|
69
|
+
return nil
|
70
|
+
end
|
71
|
+
else
|
72
|
+
return node.content.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Brewscribe
|
2
|
+
class Yeast
|
3
|
+
attr_reader :name, :lab, :product_id, :flocculation, :type, :form,
|
4
|
+
:starter_size, :amount, :price, :in_recipe, :brew_date, :pkg_date,
|
5
|
+
:cells, :min_attenuation, :max_attenuation, :min_temp, :max_temp,
|
6
|
+
:use_starter, :add_to_secondary, :times_cultured, :max_reuse,
|
7
|
+
:culture_date, :best_for, :notes, :last_modified
|
8
|
+
|
9
|
+
include Brewscribe::Conversion
|
10
|
+
|
11
|
+
FLOCCULATION_TYPES = ['Low', 'Medium', 'High', 'Very High']
|
12
|
+
TYPES = ['Ale', 'Lager', 'Wine', 'Champagne', 'Wheat']
|
13
|
+
FORMS = ['Liquid', 'Dry', 'Slant', 'Culture']
|
14
|
+
KEY_CONVERSION = {
|
15
|
+
product_id: INT_CONV,
|
16
|
+
flocculation: ->(k) { FLOCCULATION_TYPES[k.to_i] },
|
17
|
+
type: ->(k) { TYPES[k.to_i] },
|
18
|
+
last_modified: DATE_CONV,
|
19
|
+
form: ->(k) { FORMS[k.to_i] },
|
20
|
+
starter_size: FLOAT_CONV,
|
21
|
+
amount: FLOAT_CONV,
|
22
|
+
price: FLOAT_CONV,
|
23
|
+
in_recipe: BOOLEAN_CONV,
|
24
|
+
brew_date: DATE_CONV,
|
25
|
+
pkg_date: DATE_CONV,
|
26
|
+
cells: FLOAT_CONV,
|
27
|
+
min_attenuation: PERCENT_CONV,
|
28
|
+
max_attenuation: PERCENT_CONV,
|
29
|
+
min_temp: FLOAT_CONV,
|
30
|
+
max_temp: FLOAT_CONV,
|
31
|
+
use_starter: BOOLEAN_CONV,
|
32
|
+
add_to_secondary: BOOLEAN_CONV,
|
33
|
+
times_cultured: INT_CONV,
|
34
|
+
max_reuse: INT_CONV,
|
35
|
+
culture_date: DATE_CONV
|
36
|
+
}
|
37
|
+
|
38
|
+
def initialize data
|
39
|
+
@original_data = data
|
40
|
+
|
41
|
+
data_to_properties data
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewscribe do
|
4
|
+
let(:recipe_file) { File.open(File.dirname(__FILE__) + '/support/recipe.bsmx', 'r') }
|
5
|
+
|
6
|
+
describe '#import' do
|
7
|
+
before do
|
8
|
+
Brewscribe::Recipe.any_instance.stub(:parse_raw_data)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should call #read on the passed IO object' do
|
12
|
+
file = double()
|
13
|
+
file.should_receive(:read)
|
14
|
+
Brewscribe.import(file)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return a Recipe object' do
|
18
|
+
file = double()
|
19
|
+
file.stub(:read)
|
20
|
+
recipe = Brewscribe.import(file)
|
21
|
+
recipe.should be_a(Brewscribe::Recipe)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|