Created
August 4, 2013 17:34
-
-
Save doomspork/6151107 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SandPaper | |
class NoValueForRequiredField < Exception; end | |
class UnsupportedBooleanValue < Exception; end | |
TYPE_CONVERTERS = [ :boolean, :string, :integer, :float] | |
TYPE_CONVERTERS.each do |type| | |
define_method type, ->(name, options = {}) { | |
options[:xpath] ||= "//#{name.to_s}" | |
@grain[name] = options.merge type: type | |
} | |
private type | |
end | |
def initialize(&block) | |
@grain = {} | |
instance_eval &block | |
@grain.freeze | |
end | |
def refine(element, as_struct = false) | |
refined_hash = @grain.inject({}) do |memo, (name, options)| | |
node = element.at_xpath(options[:xpath]) | |
if node.nil? | |
raise NoValueForRequiredField if required?(name) | |
else | |
value = node.content.to_s | |
type = options[:type] || :string | |
memo[name] = convert_to_type(value, type) | |
end | |
memo | |
end | |
if as_struct | |
require 'ostruct' | |
return OpenStruct.new refined_hash | |
end | |
refined_hash | |
end | |
private | |
def required?(field) | |
field = @grain[field] | |
!field.key? :optional || field[:optional] == false | |
end | |
def convert_to_type(value, type) | |
method_name = "#{type.to_s.downcase}_converter".to_sym | |
self.send(method_name, value) | |
end | |
def string_converter(value) | |
value.strip | |
end | |
def boolean_converter(value) | |
result = value =~ /y(es)?|t(rue)?|1/i | |
if result.nil? | |
result = value =~ /n(o)?|f(alse)?|0/i | |
# Do this otherwise nil will be converted to false, which may be unintentional | |
raise UnsupportedBooleanValue if result.nil? | |
# Don't convert this to a boolean until we've done our final nil check | |
result = !result | |
end | |
!!result | |
end | |
def integer_converter(value) | |
value.to_i | |
end | |
def float_converter(value) | |
value.to_f | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rubygems' | |
require 'nokogiri' | |
require 'pry' | |
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'sand_paper.rb') | |
describe SandPaper do | |
let(:xml) { | |
"<element> | |
<name>Test</name> | |
<boolean>yes</boolean> | |
<deep> | |
<number>123</apth> | |
</deep> | |
<float>12.21</float> | |
</element>" | |
} | |
let(:element) { Nokogiri::XML(xml) } | |
subject { | |
SandPaper.new do | |
string :name | |
integer :number, xpath: '//deep/number' | |
boolean :truefalse, xpath: '//boolean' | |
boolean :exists, optional: true | |
boolean :does_not, optional: true | |
float :float | |
end | |
} | |
let(:refined) { subject.refine(element) } | |
describe 'dynamic methods are private' do | |
it { should_not respond_to :name } | |
it { should_not respond_to :number } | |
it { should_not respond_to :truefalse } | |
it { should_not respond_to :float } | |
end | |
describe 'OpenStruct support' do | |
let(:refined) { subject.refine(element, true) } | |
it 'creates an OpenStruct' do | |
refined.should be_kind_of OpenStruct | |
refined.should respond_to :name | |
refined.name.should eql 'Test' | |
end | |
end | |
describe 'required value support' do | |
let(:missing_required_xml) { "<optionals></optionals>" } | |
let(:paper) { | |
SandPaper.new do | |
string :required | |
end | |
} | |
it 'supports required values' do | |
expect { refined = paper.refine(Nokogiri::XML(missing_required_xml)) }.to raise_error SandPaper::NoValueForRequiredField | |
end | |
end | |
describe 'optional value support' do | |
let(:optional_xml) { | |
"<optionals> | |
<exists>string</exists> | |
</optionals>" | |
} | |
let(:paper) { | |
SandPaper.new do | |
string :exists, optional: true | |
string :does_not, optional: true | |
end | |
} | |
it 'does not error for missing optional values' do | |
refined = paper.refine(Nokogiri::XML(optional_xml), true) | |
refined.exists.should_not be_nil | |
refined.does_not.should be_nil | |
end | |
end | |
describe '.refine' do | |
describe 'type conversion' do | |
it 'supports :integer' do | |
number = refined[:number] | |
number.should be_kind_of Fixnum | |
number.should eql 123 | |
end | |
describe 'supports :boolean' do | |
let(:boolean_xml) { | |
"<boolean> | |
<yes>yes</yes> | |
<no>no</no> | |
<true>true</true> | |
<false>false</false> | |
<one>1</one> | |
<zero>0</zero> | |
</boolean>" | |
} | |
let(:paper) { | |
SandPaper.new do | |
boolean :yes | |
boolean :no, optional: true | |
boolean :true, optional: true | |
boolean :false, optional: true | |
boolean :one, optional: true | |
boolean :zero, optional: true | |
end | |
} | |
let(:refined) { paper.refine(Nokogiri::XML(boolean_xml)) } | |
it 'considers true to be: yes, true, 1' do | |
refined[:yes].should eql true | |
refined[:true].should eql true | |
refined[:one].should eql true | |
end | |
it 'considers false to be: no, false, 0' do | |
refined[:no].should eql false | |
refined[:false].should eql false | |
refined[:zero].should eql false | |
end | |
it 'raises error for all other values' do | |
binding.pry | |
expect { paper.refine(Nokogiri::XML('<boolean><yes>ok</yes></boolean>')) }.to raise_error SandPaper::UnsupportedBooleanValue | |
end | |
end | |
it 'supports :floats' do | |
refined[:float].should be_kind_of Float | |
refined[:float].should eql 12.21 | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment