api_smash 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/api_smash.rb +214 -0
  2. data/lib/version/version.rb +3 -0
  3. 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
@@ -0,0 +1,3 @@
1
+ class ApiSmash < Hashie::Dash
2
+ VERSION = "1.0.0".freeze
3
+ 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
+