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 CHANGED
@@ -1,2 +1,5 @@
1
- v0.1.0 First release, a rewrite of Shopify's own Rails plugin, turned into a gem with similar workings.
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
- module Shopify
6
- include HTTParty
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
- if [host, key, secret, token].all?
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
- child_of Blog
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
- child_of Order
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
- child_of Product
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
- child_of Product
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
- child_of Country
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
- child_of Order
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(/&lt;/, "<").
296
+ gsub(/&gt;/, ">").
297
+ gsub(/&quot;/, '"').
298
+ gsub(/&apos;/, "'").
299
+ gsub(/&amp;/, "&")
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