dcparker-shopify 0.1.9 → 0.2.0
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/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
|