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.
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
+