Skip to content

Instantly share code, notes, and snippets.

@doomspork
Created August 4, 2013 17:34
Show Gist options
  • Save doomspork/6151107 to your computer and use it in GitHub Desktop.
Save doomspork/6151107 to your computer and use it in GitHub Desktop.
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
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