dcparker-shopify 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -1
- data/Manifest +13 -1
- data/README.textile +19 -0
- data/lib/shopify.rb +46 -13
- data/lib/shopify/extlib.rb +9 -0
- data/lib/shopify/extlib/assertions.rb +8 -0
- data/lib/shopify/extlib/class.rb +98 -0
- data/lib/shopify/extlib/hash.rb +327 -0
- data/lib/shopify/extlib/hook.rb +366 -0
- data/lib/shopify/extlib/inflection.rb +436 -0
- data/lib/shopify/extlib/logger.rb +202 -0
- data/lib/shopify/extlib/object.rb +162 -0
- data/lib/shopify/extlib/pathname.rb +15 -0
- data/lib/shopify/extlib/rubygems.rb +38 -0
- data/lib/shopify/extlib/string.rb +32 -0
- data/lib/shopify/extlib/time.rb +41 -0
- data/lib/shopify/support.rb +29 -46
- data/shopify.gemspec +10 -8
- metadata +35 -8
- data/README +0 -9
data/CHANGELOG
CHANGED
@@ -1,2 +1,5 @@
|
|
1
|
-
v0.
|
1
|
+
v0.2.0 Second release, using HTTParty, capable of multiple shop connections in one app, and thread-safe.
|
2
|
+
|
2
3
|
v0.1.9 2.0 Beta, another rewrite using the light HTTParty instead of ActiveResource.
|
4
|
+
|
5
|
+
v0.1.0 First release, a rewrite of Shopify's own Rails plugin, turned into a gem with similar workings.
|
data/Manifest
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
CHANGELOG
|
2
|
+
lib/shopify/extlib/assertions.rb
|
3
|
+
lib/shopify/extlib/class.rb
|
4
|
+
lib/shopify/extlib/hash.rb
|
5
|
+
lib/shopify/extlib/hook.rb
|
6
|
+
lib/shopify/extlib/inflection.rb
|
7
|
+
lib/shopify/extlib/logger.rb
|
8
|
+
lib/shopify/extlib/object.rb
|
9
|
+
lib/shopify/extlib/pathname.rb
|
10
|
+
lib/shopify/extlib/rubygems.rb
|
11
|
+
lib/shopify/extlib/string.rb
|
12
|
+
lib/shopify/extlib/time.rb
|
13
|
+
lib/shopify/extlib.rb
|
2
14
|
lib/shopify/support.rb
|
3
15
|
lib/shopify.rb
|
4
16
|
LICENSE
|
5
17
|
Manifest
|
6
|
-
README
|
18
|
+
README.textile
|
7
19
|
shopify.gemspec
|
data/README.textile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
h1. Shopify Rubygem
|
2
|
+
|
3
|
+
* Read any kind of data from Shopify, but no support built-in yet to save data back to Shopify.
|
4
|
+
* Connect to multiple shops in the same app.
|
5
|
+
* Thread-safe.
|
6
|
+
|
7
|
+
Example Usage:
|
8
|
+
|
9
|
+
<pre>
|
10
|
+
shop = Shopify.new('store_name', 'api-key', 'api-secret', 'auth-token')
|
11
|
+
order = shop.orders(:limit => 1)[0] # => gets first order
|
12
|
+
order.line_items # => the line items within that order
|
13
|
+
order.fulfillments # => gets all fulfillments related to this order
|
14
|
+
blogs = shop.blogs # => gets all blogs for this shop
|
15
|
+
articles = blogs[0].articles # => gets all the articles in this blog
|
16
|
+
articles[0].comments # => gets the comments for that article
|
17
|
+
shop.products # => get all products in this shop
|
18
|
+
... and much more ... :)
|
19
|
+
</pre>
|
data/lib/shopify.rb
CHANGED
@@ -2,24 +2,52 @@ include_path = File.expand_path(File.dirname(__FILE__))
|
|
2
2
|
$:.unshift(include_path) unless $:.include?(include_path)
|
3
3
|
require 'shopify/support'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# Class: Shopify
|
6
|
+
# Usage:
|
7
|
+
# shop = Shopify.new(host, [key, [secret, [token]]])
|
8
|
+
# shop.orders
|
9
|
+
# TODO: Make the object remember the results of queries such as shop.orders when called without parameters,
|
10
|
+
# and reload only when you call shop.orders(true)
|
11
|
+
class Shopify
|
12
|
+
attr_reader :host
|
13
|
+
|
14
|
+
def initialize(host, key=nil, secret=nil, token=nil)
|
15
|
+
@default_options = {}
|
16
|
+
extend HTTParty::ClassMethods
|
7
17
|
|
8
|
-
def self.setup(host, key=nil, secret=nil, token=nil)
|
9
18
|
host.gsub!(/https?:\/\//, '') # remove http(s)://
|
10
19
|
@host = host.include?('.') ? host : "#{host}.myshopify.com" # extend url to myshopify.com if no host is given
|
11
20
|
@key = key
|
12
21
|
@secret = secret
|
13
22
|
@token = token
|
14
|
-
|
23
|
+
setup
|
24
|
+
end
|
25
|
+
|
26
|
+
def needs_authorization?
|
27
|
+
![@host, @key, @secret, @token].all?
|
28
|
+
end
|
29
|
+
|
30
|
+
def authorize!(token)
|
31
|
+
@token = token
|
32
|
+
setup
|
33
|
+
end
|
34
|
+
|
35
|
+
def authorization_url(mode='w')
|
36
|
+
"http://#{@host}/admin/api/auth?api_key=#{@key}&mode=#{mode}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup
|
40
|
+
unless needs_authorization?
|
15
41
|
base_uri "http://#{@host}/admin"
|
16
42
|
basic_auth @key, Digest::MD5.hexdigest("#{@secret.chomp}#{@token.chomp}")
|
17
43
|
format :xml
|
18
|
-
return false
|
19
|
-
else
|
20
|
-
"http://#{@host}/admin/api/auth?api_key=#{@key}&mode=#{mode}"
|
21
44
|
end
|
22
45
|
end
|
46
|
+
private :setup
|
47
|
+
|
48
|
+
##############################
|
49
|
+
## Shopify Object Classes ##
|
50
|
+
##############################
|
23
51
|
|
24
52
|
# /admin/blogs.xml
|
25
53
|
class Blog < ShopifyModel
|
@@ -33,7 +61,7 @@ module Shopify
|
|
33
61
|
|
34
62
|
# /admin/blogs/[blog_id]/articles.xml
|
35
63
|
class Article < ShopifyModel
|
36
|
-
|
64
|
+
children_of Blog
|
37
65
|
attr_accessor :author, :blog_id, :body, :body_html, :created_at, :id, :published_at, :title, :updated_at
|
38
66
|
def comments(query_params={})
|
39
67
|
Shopify.comments(query_params.merge(:article_id => id, :blog_id => blog_id))
|
@@ -85,9 +113,14 @@ module Shopify
|
|
85
113
|
end
|
86
114
|
end
|
87
115
|
|
116
|
+
class LineItem < ShopifyModel
|
117
|
+
children_of Order
|
118
|
+
attr_accessor :fulfillment_service, :grams, :id, :price, :quantity, :sku, :title, :variant_id, :vendor, :name, :product_title
|
119
|
+
end
|
120
|
+
|
88
121
|
# /admin/orders/[order_id]/fulfillments.xml
|
89
122
|
class Fulfillment < ShopifyModel
|
90
|
-
|
123
|
+
children_of Order
|
91
124
|
attr_accessor :id, :order_id, :status, :tracking_number, :line_items, :receipt
|
92
125
|
end
|
93
126
|
|
@@ -113,19 +146,19 @@ module Shopify
|
|
113
146
|
|
114
147
|
# /admin/products/[product_id]/images.xml
|
115
148
|
class Image < ShopifyModel
|
116
|
-
|
149
|
+
children_of Product
|
117
150
|
attr_accessor :id, :position, :product_id, :src
|
118
151
|
end
|
119
152
|
|
120
153
|
# /admin/products/[product_id]/variants.xml
|
121
154
|
class Variant < ShopifyModel
|
122
|
-
|
155
|
+
children_of Product
|
123
156
|
attr_accessor :compare_at_price, :fulfillment_service, :grams, :id, :inventory_management, :inventory_policy, :inventory_quantity, :position, :price, :product_id, :sku, :title
|
124
157
|
end
|
125
158
|
|
126
159
|
# /admin/countries/[country_id]/provinces.xml
|
127
160
|
class Province < ShopifyModel
|
128
|
-
|
161
|
+
children_of Country
|
129
162
|
attr_accessor :code, :id, :name, :tax
|
130
163
|
end
|
131
164
|
|
@@ -143,7 +176,7 @@ module Shopify
|
|
143
176
|
|
144
177
|
# /admin/orders/[order_id]/transactions.xml
|
145
178
|
class Transaction < ShopifyModel
|
146
|
-
|
179
|
+
children_of Order
|
147
180
|
attr_accessor :amount, :authorization, :created_at, :kind, :order_id, :status, :receipt
|
148
181
|
end
|
149
182
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# :nodoc:all
|
2
|
+
|
3
|
+
require 'quickbooks/extlib/class'
|
4
|
+
require 'quickbooks/extlib/object'
|
5
|
+
require 'quickbooks/extlib/string'
|
6
|
+
require 'quickbooks/extlib/hash'
|
7
|
+
require 'quickbooks/extlib/time'
|
8
|
+
require 'quickbooks/extlib/assertions'
|
9
|
+
require 'quickbooks/extlib/inflection'
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Extlib # :nodoc:all
|
2
|
+
module Assertions
|
3
|
+
def assert_kind_of(name, value, *klasses)
|
4
|
+
klasses.each { |k| return if value.kind_of?(k) }
|
5
|
+
raise ArgumentError, "+#{name}+ should be #{klasses.map { |k| k.name } * ' or '}, but was #{value.class.name}", caller(2)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Copyright (c) 2004-2008 David Heinemeier Hansson
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
# Allows attributes to be shared within an inheritance hierarchy, but where
|
23
|
+
# each descendant gets a copy of their parents' attributes, instead of just a
|
24
|
+
# pointer to the same. This means that the child can add elements to, for
|
25
|
+
# example, an array without those additions being shared with either their
|
26
|
+
# parent, siblings, or children, which is unlike the regular class-level
|
27
|
+
# attributes that are shared across the entire hierarchy.
|
28
|
+
class Class # :nodoc:all
|
29
|
+
# Defines class-level and instance-level attribute reader.
|
30
|
+
#
|
31
|
+
# @param *syms<Array> Array of attributes to define reader for.
|
32
|
+
# @return <Array[#to_s]> List of attributes that were made into cattr_readers
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
#
|
36
|
+
# @todo Is this inconsistent in that it does not allow you to prevent
|
37
|
+
# an instance_reader via :instance_reader => false
|
38
|
+
def cattr_reader(*syms)
|
39
|
+
syms.flatten.each do |sym|
|
40
|
+
next if sym.is_a?(Hash)
|
41
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
42
|
+
unless defined? @@#{sym}
|
43
|
+
@@#{sym} = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.#{sym}
|
47
|
+
@@#{sym}
|
48
|
+
end
|
49
|
+
|
50
|
+
def #{sym}
|
51
|
+
@@#{sym}
|
52
|
+
end
|
53
|
+
RUBY
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Defines class-level (and optionally instance-level) attribute writer.
|
58
|
+
#
|
59
|
+
# @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
|
60
|
+
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
|
61
|
+
# @return <Array[#to_s]> List of attributes that were made into cattr_writers
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def cattr_writer(*syms)
|
65
|
+
options = syms.last.is_a?(Hash) ? syms.pop : {}
|
66
|
+
syms.flatten.each do |sym|
|
67
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
68
|
+
unless defined? @@#{sym}
|
69
|
+
@@#{sym} = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.#{sym}=(obj)
|
73
|
+
@@#{sym} = obj
|
74
|
+
end
|
75
|
+
RUBY
|
76
|
+
|
77
|
+
unless options[:instance_writer] == false
|
78
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
79
|
+
def #{sym}=(obj)
|
80
|
+
@@#{sym} = obj
|
81
|
+
end
|
82
|
+
RUBY
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Defines class-level (and optionally instance-level) attribute accessor.
|
88
|
+
#
|
89
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
|
90
|
+
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
|
91
|
+
# @return <Array[#to_s]> List of attributes that were made into accessors
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
def cattr_accessor(*syms)
|
95
|
+
cattr_reader(*syms)
|
96
|
+
cattr_writer(*syms)
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
class Hash # :nodoc:all
|
2
|
+
##
|
3
|
+
# Convert to URL query param string
|
4
|
+
#
|
5
|
+
# { :name => "Bob",
|
6
|
+
# :address => {
|
7
|
+
# :street => '111 Ruby Ave.',
|
8
|
+
# :city => 'Ruby Central',
|
9
|
+
# :phones => ['111-111-1111', '222-222-2222']
|
10
|
+
# }
|
11
|
+
# }.to_params
|
12
|
+
# #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
|
13
|
+
#
|
14
|
+
# @return [String] This hash as a query string
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def to_params
|
18
|
+
params = self.map { |k,v| normalize_param(k,v) }.join
|
19
|
+
params.chop! # trailing &
|
20
|
+
params
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Convert a key, value pair into a URL query param string
|
25
|
+
#
|
26
|
+
# normalize_param(:name, "Bob") #=> "name=Bob&"
|
27
|
+
#
|
28
|
+
# @param [Object] key The key for the param.
|
29
|
+
# @param [Object] value The value for the param.
|
30
|
+
#
|
31
|
+
# @return <String> This key value pair as a param
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def normalize_param(key, value)
|
35
|
+
param = ''
|
36
|
+
stack = []
|
37
|
+
|
38
|
+
if value.is_a?(Array)
|
39
|
+
param << value.map { |element| normalize_param("#{key}[]", element) }.join
|
40
|
+
elsif value.is_a?(Hash)
|
41
|
+
stack << [key,value]
|
42
|
+
else
|
43
|
+
param << "#{key}=#{value}&"
|
44
|
+
end
|
45
|
+
|
46
|
+
stack.each do |parent, hash|
|
47
|
+
hash.each do |key, value|
|
48
|
+
if value.is_a?(Hash)
|
49
|
+
stack << ["#{parent}[#{key}]", value]
|
50
|
+
else
|
51
|
+
param << normalize_param("#{parent}[#{key}]", value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
param
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Create a hash with *only* key/value pairs in receiver and +allowed+
|
61
|
+
#
|
62
|
+
# { :one => 1, :two => 2, :three => 3 }.only(:one) #=> { :one => 1 }
|
63
|
+
#
|
64
|
+
# @param [Array[String, Symbol]] *allowed The hash keys to include.
|
65
|
+
#
|
66
|
+
# @return [Hash] A new hash with only the selected keys.
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
def only(*allowed)
|
70
|
+
hash = {}
|
71
|
+
allowed.each {|k| hash[k] = self[k] if self.has_key?(k) }
|
72
|
+
hash
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Create a hash with all key/value pairs in receiver *except* +rejected+
|
77
|
+
#
|
78
|
+
# { :one => 1, :two => 2, :three => 3 }.except(:one)
|
79
|
+
# #=> { :two => 2, :three => 3 }
|
80
|
+
#
|
81
|
+
# @param [Array[String, Symbol]] *rejected The hash keys to exclude.
|
82
|
+
#
|
83
|
+
# @return [Hash] A new hash without the selected keys.
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def except(*rejected)
|
87
|
+
hash = self.dup
|
88
|
+
rejected.each {|k| hash.delete(k) }
|
89
|
+
hash
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return <String> The hash as attributes for an XML tag.
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# { :one => 1, "two"=>"TWO" }.to_xml_attributes
|
96
|
+
# #=> 'one="1" two="TWO"'
|
97
|
+
def to_xml_attributes
|
98
|
+
map do |k,v|
|
99
|
+
%{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
|
100
|
+
end.join(' ')
|
101
|
+
end
|
102
|
+
|
103
|
+
alias_method :to_html_attributes, :to_xml_attributes
|
104
|
+
|
105
|
+
# @param html_class<#to_s>
|
106
|
+
# The HTML class to add to the :class key. The html_class will be
|
107
|
+
# concatenated to any existing classes.
|
108
|
+
#
|
109
|
+
# @example hash[:class] #=> nil
|
110
|
+
# @example hash.add_html_class!(:selected)
|
111
|
+
# @example hash[:class] #=> "selected"
|
112
|
+
# @example hash.add_html_class!("class1 class2")
|
113
|
+
# @example hash[:class] #=> "selected class1 class2"
|
114
|
+
def add_html_class!(html_class)
|
115
|
+
if self[:class]
|
116
|
+
self[:class] = "#{self[:class]} #{html_class}"
|
117
|
+
else
|
118
|
+
self[:class] = html_class.to_s
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Converts all keys into string values. This is used during reloading to
|
123
|
+
# prevent problems when classes are no longer declared.
|
124
|
+
#
|
125
|
+
# @return <Array> An array of they hash's keys
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
# hash = { One => 1, Two => 2 }.proctect_keys!
|
129
|
+
# hash # => { "One" => 1, "Two" => 2 }
|
130
|
+
def protect_keys!
|
131
|
+
keys.each {|key| self[key.to_s] = delete(key) }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Attempts to convert all string keys into Class keys. We run this after
|
135
|
+
# reloading to convert protected hashes back into usable hashes.
|
136
|
+
#
|
137
|
+
# @example
|
138
|
+
# # Provided that classes One and Two are declared in this scope:
|
139
|
+
# hash = { "One" => 1, "Two" => 2 }.unproctect_keys!
|
140
|
+
# hash # => { One => 1, Two => 2 }
|
141
|
+
def unprotect_keys!
|
142
|
+
keys.each do |key|
|
143
|
+
(self[Object.full_const_get(key)] = delete(key)) rescue nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Destructively and non-recursively convert each key to an uppercase string,
|
148
|
+
# deleting nil values along the way.
|
149
|
+
#
|
150
|
+
# @return <Hash> The newly environmentized hash.
|
151
|
+
#
|
152
|
+
# @example
|
153
|
+
# { :name => "Bob", :contact => { :email => "bob@bob.com" } }.environmentize_keys!
|
154
|
+
# #=> { "NAME" => "Bob", "CONTACT" => { :email => "bob@bob.com" } }
|
155
|
+
def environmentize_keys!
|
156
|
+
keys.each do |key|
|
157
|
+
val = delete(key)
|
158
|
+
next if val.nil?
|
159
|
+
self[key.to_s.upcase] = val
|
160
|
+
end
|
161
|
+
self
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
require 'rexml/parsers/streamparser'
|
166
|
+
require 'rexml/parsers/baseparser'
|
167
|
+
require 'rexml/light/node'
|
168
|
+
|
169
|
+
# This is a slighly modified version of the XMLUtilityNode from
|
170
|
+
# http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
|
171
|
+
# It's mainly just adding vowels, as I ht cd wth n vwls :)
|
172
|
+
# This represents the hard part of the work, all I did was change the
|
173
|
+
# underlying parser.
|
174
|
+
class REXMLUtilityNode # :nodoc:all
|
175
|
+
attr_accessor :name, :attributes, :children, :type
|
176
|
+
cattr_accessor :typecasts, :available_typecasts
|
177
|
+
|
178
|
+
self.typecasts = {}
|
179
|
+
self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
|
180
|
+
self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
|
181
|
+
self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
|
182
|
+
self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
|
183
|
+
self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
|
184
|
+
self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
|
185
|
+
self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
|
186
|
+
self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
|
187
|
+
self.typecasts["symbol"] = lambda{|v| v.to_sym}
|
188
|
+
self.typecasts["string"] = lambda{|v| v.to_s}
|
189
|
+
self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
|
190
|
+
self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
|
191
|
+
|
192
|
+
self.available_typecasts = self.typecasts.keys
|
193
|
+
|
194
|
+
def initialize(name, attributes = {})
|
195
|
+
@name = name.tr("-", "_")
|
196
|
+
# leave the type alone if we don't know what it is
|
197
|
+
@type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
|
198
|
+
|
199
|
+
@nil_element = attributes.delete("nil") == "true"
|
200
|
+
@attributes = undasherize_keys(attributes)
|
201
|
+
@children = []
|
202
|
+
@text = false
|
203
|
+
end
|
204
|
+
|
205
|
+
def add_node(node)
|
206
|
+
@text = true if node.is_a? String
|
207
|
+
@children << node
|
208
|
+
end
|
209
|
+
|
210
|
+
def to_hash
|
211
|
+
if @type == "file"
|
212
|
+
f = StringIO.new((@children.first || '').unpack('m').first)
|
213
|
+
class << f
|
214
|
+
attr_accessor :original_filename, :content_type
|
215
|
+
end
|
216
|
+
f.original_filename = attributes['name'] || 'untitled'
|
217
|
+
f.content_type = attributes['content_type'] || 'application/octet-stream'
|
218
|
+
return {name => f}
|
219
|
+
end
|
220
|
+
|
221
|
+
if @text
|
222
|
+
return { name => typecast_value( translate_xml_entities( inner_html ) ) }
|
223
|
+
else
|
224
|
+
#change repeating groups into an array
|
225
|
+
groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
|
226
|
+
|
227
|
+
out = nil
|
228
|
+
if @type == "array"
|
229
|
+
out = []
|
230
|
+
groups.each do |k, v|
|
231
|
+
if v.size == 1
|
232
|
+
out << v.first.to_hash.entries.first.last
|
233
|
+
else
|
234
|
+
out << v.map{|e| e.to_hash[k]}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
out = out.flatten
|
238
|
+
|
239
|
+
else # If Hash
|
240
|
+
out = {}
|
241
|
+
groups.each do |k,v|
|
242
|
+
if v.size == 1
|
243
|
+
out.merge!(v.first)
|
244
|
+
else
|
245
|
+
out.merge!( k => v.map{|e| e.to_hash[k]})
|
246
|
+
end
|
247
|
+
end
|
248
|
+
out.merge! attributes unless attributes.empty?
|
249
|
+
out = out.empty? ? nil : out
|
250
|
+
end
|
251
|
+
|
252
|
+
if @type && out.nil?
|
253
|
+
{ name => typecast_value(out) }
|
254
|
+
else
|
255
|
+
{ name => out }
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Typecasts a value based upon its type. For instance, if
|
261
|
+
# +node+ has #type == "integer",
|
262
|
+
# {{[node.typecast_value("12") #=> 12]}}
|
263
|
+
#
|
264
|
+
# @param value<String> The value that is being typecast.
|
265
|
+
#
|
266
|
+
# @details [:type options]
|
267
|
+
# "integer"::
|
268
|
+
# converts +value+ to an integer with #to_i
|
269
|
+
# "boolean"::
|
270
|
+
# checks whether +value+, after removing spaces, is the literal
|
271
|
+
# "true"
|
272
|
+
# "datetime"::
|
273
|
+
# Parses +value+ using Time.parse, and returns a UTC Time
|
274
|
+
# "date"::
|
275
|
+
# Parses +value+ using Date.parse
|
276
|
+
#
|
277
|
+
# @return <Integer, TrueClass, FalseClass, Time, Date, Object>
|
278
|
+
# The result of typecasting +value+.
|
279
|
+
#
|
280
|
+
# @note
|
281
|
+
# If +self+ does not have a "type" key, or if it's not one of the
|
282
|
+
# options specified above, the raw +value+ will be returned.
|
283
|
+
def typecast_value(value)
|
284
|
+
return value unless @type
|
285
|
+
proc = self.class.typecasts[@type]
|
286
|
+
proc.nil? ? value : proc.call(value)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Convert basic XML entities into their literal values.
|
290
|
+
#
|
291
|
+
# @param value<#gsub> An XML fragment.
|
292
|
+
#
|
293
|
+
# @return <#gsub> The XML fragment after converting entities.
|
294
|
+
def translate_xml_entities(value)
|
295
|
+
value.gsub(/</, "<").
|
296
|
+
gsub(/>/, ">").
|
297
|
+
gsub(/"/, '"').
|
298
|
+
gsub(/'/, "'").
|
299
|
+
gsub(/&/, "&")
|
300
|
+
end
|
301
|
+
|
302
|
+
# Take keys of the form foo-bar and convert them to foo_bar
|
303
|
+
def undasherize_keys(params)
|
304
|
+
params.keys.each do |key, value|
|
305
|
+
params[key.tr("-", "_")] = params.delete(key)
|
306
|
+
end
|
307
|
+
params
|
308
|
+
end
|
309
|
+
|
310
|
+
# Get the inner_html of the REXML node.
|
311
|
+
def inner_html
|
312
|
+
@children.join
|
313
|
+
end
|
314
|
+
|
315
|
+
# Converts the node into a readable HTML node.
|
316
|
+
#
|
317
|
+
# @return <String> The HTML node in text form.
|
318
|
+
def to_html
|
319
|
+
attributes.merge!(:type => @type ) if @type
|
320
|
+
"<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
|
321
|
+
end
|
322
|
+
|
323
|
+
# @alias #to_html #to_s
|
324
|
+
def to_s
|
325
|
+
to_html
|
326
|
+
end
|
327
|
+
end
|