brewscribe 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|