api_smash 1.0.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/lib/api_smash.rb +214 -0
- data/lib/version/version.rb +3 -0
- metadata +129 -0
data/lib/api_smash.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'hashie/dash'
|
2
|
+
|
3
|
+
# ApiSmash is a subclass of Hashie::Dash that adds several features
|
4
|
+
# making it suitable for use in writing api clients. Namely,
|
5
|
+
#
|
6
|
+
# * The ability to silence exceptions on unknown keys (vs. Raising NoMethodError)
|
7
|
+
# * The ability to define conversion of incoming data via transformers
|
8
|
+
# * The ability to define aliases for keys via the from parameter.
|
9
|
+
#
|
10
|
+
# It extends Hashie::Dash to suppress unknown keys when passing data, but
|
11
|
+
# is configurable to raises an UnknownKey exception when accessing keys in the
|
12
|
+
# ApiSmash.
|
13
|
+
#
|
14
|
+
# @author Darcy Laycock
|
15
|
+
# @author Steve Webb
|
16
|
+
#
|
17
|
+
# @example a simple, structured object with the most common use cases.
|
18
|
+
# class MyResponse < ApiSmash
|
19
|
+
# property :full_name, :from => :fullName
|
20
|
+
# property :value_percentage, :transformer => :to_f
|
21
|
+
# property :short_name
|
22
|
+
# property :created, :transformer => lambda { |v| Date.parse(v) }
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# response = MyResponse.new({
|
26
|
+
# :fullName => "Bob Smith",
|
27
|
+
# :value_percentage => "10.5",
|
28
|
+
# :short_name => 'Bob',
|
29
|
+
# :created => '2010-12-28'
|
30
|
+
# })
|
31
|
+
#
|
32
|
+
# p response.short_name # => "Bob"
|
33
|
+
# p response.full_name # => "Bob Smith"
|
34
|
+
# p response.value_percentage # => 10.5
|
35
|
+
# p response.created.class # => Date
|
36
|
+
#
|
37
|
+
class ApiSmash < Hashie::Dash
|
38
|
+
|
39
|
+
# When we access an unknown property, we raise the unknown key instead of
|
40
|
+
# a NoMethodError on undefined keys so that we can do a target rescue.
|
41
|
+
class UnknownKey < StandardError; end
|
42
|
+
|
43
|
+
# Returns a class-specific hash of transformers, containing the attribute
|
44
|
+
# name mapped to the transformer that responds to call.
|
45
|
+
# @return The hash of transformers.
|
46
|
+
def self.transformers
|
47
|
+
(@transformers ||= {})
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a class-specific hash of incoming keys and their resultant
|
51
|
+
# property name, useful for mapping non-standard names (e.g. displayName)
|
52
|
+
# to their more ruby-like equivelant (e.g. display_name).
|
53
|
+
# @return The hash of key mappings.
|
54
|
+
def self.key_mapping
|
55
|
+
(@key_mapping ||= {})
|
56
|
+
end
|
57
|
+
|
58
|
+
# Test if the object should raise a NoMethodError exception on unknown
|
59
|
+
# property accessors or whether it should be silenced.
|
60
|
+
#
|
61
|
+
# @return true if an exception will be raised when accessing an unknown key
|
62
|
+
# else, false.
|
63
|
+
def self.exception_on_unknown_key?
|
64
|
+
defined?(@exception_on_unknown_key) && @exception_on_unknown_key
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets whether or not ApiSmash should raise NoMethodError on an unknown key.
|
68
|
+
# Sets it for the current class.
|
69
|
+
#
|
70
|
+
# @param [Boolean] value true to throw exceptions.
|
71
|
+
def self.exception_on_unknown_key=(value)
|
72
|
+
@exception_on_unknown_key = value
|
73
|
+
end
|
74
|
+
self.exception_on_unknown_key = false
|
75
|
+
|
76
|
+
# Sets the transformer that is invoked when the given key is set.
|
77
|
+
#
|
78
|
+
# @param [Symbol] key The key should this transformer operate on
|
79
|
+
# @param [#call] value If a block isn't given, used to transform via #call.
|
80
|
+
# @param [Block] blk The block used to transform the key.
|
81
|
+
def self.transformer_for(key, value = nil, &blk)
|
82
|
+
if blk.nil? && value
|
83
|
+
blk = value.respond_to?(:call) ? value : value.to_sym.to_proc
|
84
|
+
end
|
85
|
+
raise ArgumentError, 'must provide a transformation' if blk.nil?
|
86
|
+
transformers[key.to_s] = blk
|
87
|
+
# For each subclass, set the transformer.
|
88
|
+
Array(@subclasses).each { |klass| klass.transformer_for(key, value) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Hook to make it inherit instance variables correctly. Called once
|
92
|
+
# the ApiSmash is inherited from in another object to maintain state.
|
93
|
+
def self.inherited(klass)
|
94
|
+
super
|
95
|
+
klass.instance_variable_set '@transformers', transformers.dup
|
96
|
+
klass.instance_variable_set '@key_mapping', key_mapping.dup
|
97
|
+
klass.instance_variable_set '@exception_on_unknown_key', exception_on_unknown_key?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create a new property (i.e., hash key) for this Object type. This method
|
101
|
+
# allows for converting property names and defining custom transformers for
|
102
|
+
# more complex types.
|
103
|
+
#
|
104
|
+
# @param [Symbol] property_name The property name (duh).
|
105
|
+
# @param [Hash] options
|
106
|
+
# @option options [String, Array<String>] :from Also accept values for this property when
|
107
|
+
# using the key(s) specified in from.
|
108
|
+
# @option options [Block] :transformer Specify a class or block to use when transforming the data.
|
109
|
+
def self.property(property_name, options = {})
|
110
|
+
super
|
111
|
+
if options[:from]
|
112
|
+
property_name = property_name.to_s
|
113
|
+
Array(options[:from]).each do |k|
|
114
|
+
key_mapping[k.to_s] = property_name
|
115
|
+
end
|
116
|
+
end
|
117
|
+
if options[:transformer]
|
118
|
+
transformer_for property_name, options[:transformer]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Does this ApiSmash class contain a specific property (key),
|
123
|
+
# or does it have a key mapping (via :from)
|
124
|
+
#
|
125
|
+
# @param [Symbol] key the property to test for.
|
126
|
+
# @return [Boolean] true if this class contains the key; else, false.
|
127
|
+
def self.property?(key)
|
128
|
+
super || key_mapping.has_key?(key.to_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Automates type conversion (including on Array and Hashes) to this type.
|
132
|
+
# Used so we can pass this class similarily to how we pass lambdas as an
|
133
|
+
# object, primarily for use as transformers.
|
134
|
+
#
|
135
|
+
# @param [Object] the object to attempt to convert.
|
136
|
+
# @return [Array<ApiSmash>, ApiSmash] The converted object / array of objects if
|
137
|
+
# possible, otherwise nil.
|
138
|
+
def self.call(value)
|
139
|
+
if value.is_a?(Array)
|
140
|
+
value.map { |v| call v }.compact
|
141
|
+
elsif value.is_a?(Hash)
|
142
|
+
new value
|
143
|
+
else
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
class << self
|
148
|
+
alias_method :transform, :call
|
149
|
+
end
|
150
|
+
|
151
|
+
# Access the value responding to a key, normalising the key into a form
|
152
|
+
# we know (e.g. processing the from value to convert it to the actual
|
153
|
+
# property name).
|
154
|
+
#
|
155
|
+
# @param [Symbol] property the key to check for.
|
156
|
+
# @return The value corresponding to property. nil if it does not exist.
|
157
|
+
def [](property)
|
158
|
+
super transform_key(property)
|
159
|
+
rescue UnknownKey
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# Sets the value for a given key. Transforms the key first (e.g. taking into
|
164
|
+
# account from values) and transforms the property using any transformers.
|
165
|
+
#
|
166
|
+
# @param [Symbol] property the key to set.
|
167
|
+
# @param [String] value the value to set.
|
168
|
+
# @return If the property exists value is returned; else, nil.
|
169
|
+
def []=(property, value)
|
170
|
+
key = transform_key(property)
|
171
|
+
super key, transform_property(key, value)
|
172
|
+
rescue UnknownKey
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# Overrides the Dashie check to raise a custom exception that we can
|
179
|
+
# rescue from when the key is unknown.
|
180
|
+
def assert_property_exists!(property)
|
181
|
+
has_property = self.class.property?(property)
|
182
|
+
unless has_property
|
183
|
+
exception = self.class.exception_on_unknown_key? ? NoMethodError : UnknownKey
|
184
|
+
raise exception, "The property '#{property}' is not defined on this #{self.class.name}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Transforms a given key into it's normalised alternative, making it
|
189
|
+
# suitable for automatically mapping external objects into a useable
|
190
|
+
# local version.
|
191
|
+
# @param [Symbol, String] key the starting key, pre-transformation
|
192
|
+
# @return [String] the transformed key, ready for use internally.
|
193
|
+
def transform_key(key)
|
194
|
+
self.class.key_mapping[key.to_s] || default_key_transformation(key)
|
195
|
+
end
|
196
|
+
|
197
|
+
# By default, we transform the key using #to_s, making it useable
|
198
|
+
# as a hash index. If you want to, for example, add leading underscores,
|
199
|
+
# you're do so here.
|
200
|
+
def default_key_transformation(key)
|
201
|
+
key.to_s
|
202
|
+
end
|
203
|
+
|
204
|
+
# Given a key and a value, applies any incoming data transformations as appropriate.
|
205
|
+
# @param [String, Symbol] key the property key
|
206
|
+
# @param [Object] value the incoming value of the given property
|
207
|
+
# @return [Object] the transformed value for the given key
|
208
|
+
# @see ApiSmash.transformer_for
|
209
|
+
def transform_property(key, value)
|
210
|
+
transformation = self.class.transformers[key.to_s]
|
211
|
+
transformation ? transformation.call(value) : value
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_smash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Darcy Laycock
|
14
|
+
- Steve Webb
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-08-23 00:00:00 +09:30
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: hashie
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 15
|
31
|
+
segments:
|
32
|
+
- 1
|
33
|
+
- 0
|
34
|
+
version: "1.0"
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rr
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rspec
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 2
|
62
|
+
- 0
|
63
|
+
version: "2.0"
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: fuubar
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id004
|
80
|
+
description: Hashie dash api_smash
|
81
|
+
email:
|
82
|
+
- sutto@thefrontiergroup.com.au
|
83
|
+
executables: []
|
84
|
+
|
85
|
+
extensions: []
|
86
|
+
|
87
|
+
extra_rdoc_files: []
|
88
|
+
|
89
|
+
files:
|
90
|
+
- lib/api_smash.rb
|
91
|
+
- lib/version/version.rb
|
92
|
+
has_rdoc: true
|
93
|
+
homepage: http://github.com/thefrontiergroup
|
94
|
+
licenses: []
|
95
|
+
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 3
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 23
|
116
|
+
segments:
|
117
|
+
- 1
|
118
|
+
- 3
|
119
|
+
- 6
|
120
|
+
version: 1.3.6
|
121
|
+
requirements: []
|
122
|
+
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 1.6.2
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: A dash with transformers
|
128
|
+
test_files: []
|
129
|
+
|