api-resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5dae0339d240421eb712387cb2a22f95da96640
4
+ data.tar.gz: ec15e6fe4b2179873490059e11ca1811823ddee1
5
+ SHA512:
6
+ metadata.gz: 1be26aca4ea7a90274e8ebe02dec7c3c18bcc51df974dfee5c3e9f471464cf490e5e68405e6cfd55155e18055d23c01cd7b458e8341aed78235476b005822b96
7
+ data.tar.gz: e5daac829ddb90523e31364302d53eec67046474c2ae8915921245828b0c6a50079e61fcaf2a2a9d1c38f3bc0c950792139f4b593667f1f39ad26e1b25832069
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2015 Wizypay - Chaker Nakhli
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Api::Resource
2
+
3
+ Library for accessing Restful APIs.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'api-resource'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install api-resource
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,113 @@
1
+ require 'rest_client'
2
+ require 'active_model'
3
+ require 'active_model/model'
4
+ require 'active_model/serializers/json'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+ require 'active_support/core_ext/object/to_param'
7
+
8
+ class Resource
9
+
10
+ include ActiveModel::Model
11
+ include ActiveModel::Serializers::JSON
12
+
13
+ class_attribute :base_url
14
+ class_attribute :hmac
15
+ class_attribute :api_id
16
+ class_attribute :api_secret
17
+ class_attribute :auth_prefix
18
+
19
+ def self.with_hmac(api_id, api_secret, auth_prefix='APIAuth')
20
+ self.hmac = true
21
+ self.api_id = api_id
22
+ self.api_secret = api_secret
23
+ self.auth_prefix = auth_prefix
24
+ end
25
+
26
+ def initialize(hash=nil)
27
+ self.attributes = hash if hash
28
+ end
29
+
30
+ def self.resource_name
31
+ self.to_s.tableize
32
+ end
33
+
34
+ def self.find(id)
35
+ result = client(:get, {}, resource_name, id)
36
+ self.new.from_json(result)
37
+ end
38
+
39
+ def self.all
40
+ result = client(:get, {}, resource_name)
41
+ array = JSON.parse(result)
42
+ array.map { |h| self.new(h) }
43
+ end
44
+
45
+ def self.where(options, verb=:get)
46
+ result = client(verb, *(verb==:get ? [{}, "#{resource_name}?#{options.to_param}"] : [options, resource_name]))
47
+ array = JSON.parse(result)
48
+ array.map { |h| self.new(h) }
49
+ end
50
+
51
+ def attributes
52
+ instance_values.with_indifferent_access
53
+ end
54
+
55
+ protected
56
+
57
+ def attributes=(hash)
58
+ hash.each do |key, value|
59
+ if respond_to? "#{key}="
60
+ send("#{key}=", value)
61
+ else
62
+ child_class = self.class.load_class(key)
63
+ if child_class && child_class < Resource
64
+ if value.is_a?(Hash)
65
+ child_value = child_class.new(value)
66
+ define_singleton_method(key) { child_value }
67
+ elsif value.is_a?(Array)
68
+ child_values = value.map { |h| child_class.new(h) }
69
+ define_singleton_method(key) { child_values }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.method_missing(m, *args, &_)
77
+ unless args.empty?
78
+ by_method_match = /^by_(.+)/.match(m)
79
+ if by_method_match
80
+ parent_resource_name = by_method_match[1]
81
+ with_parent_resource(args[0], parent_resource_name)
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.load_class(plural_resource_name)
87
+ plural_resource_name.to_s.singularize.classify.constantize rescue nil
88
+ end
89
+
90
+ def self.with_parent_resource(parent_resource_id, parent_resource_name)
91
+ result = client(:get, {}, parent_resource_name.pluralize, parent_resource_id, self.resource_name)
92
+ hashes = JSON.parse(result)
93
+ hashes.map { |h| self.new(h) }
94
+ end
95
+
96
+ def self.client(verb, params, *paths)
97
+ url = base_url
98
+ url += '/' unless url.end_with?('/')
99
+ url = url + paths.join('/')
100
+ headers = { accept: :json }
101
+ req_params = { url: url, method: verb, headers: headers }
102
+ if verb == :get
103
+ headers[:params] = params
104
+ else
105
+ req_params[:payload] = params
106
+ end
107
+ req = RestClient::Request.new(req_params)
108
+ if hmac
109
+ req.sign!(api_id, api_secret, auth_prefix)
110
+ end
111
+ req.execute
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module ApiResource
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1 @@
1
+ require_relative 'api-resource/resource'
@@ -0,0 +1,142 @@
1
+ RSpec.describe Resource do
2
+
3
+ class BlogResource < Resource
4
+ self.base_url = 'http://api.example.com'
5
+ end
6
+
7
+ class Category < BlogResource
8
+ attr_accessor :id, :name
9
+ end
10
+
11
+ class Blog < BlogResource
12
+ attr_accessor :id, :title
13
+ end
14
+
15
+ class Tag < BlogResource
16
+ attr_accessor :id, :name
17
+ end
18
+
19
+ context '#find' do
20
+ it 'creates the resource when found' do
21
+ resource_values = { 'id' => 1257, 'title' => 'Hello' }
22
+ resource_id = 3
23
+ stub_request(:get, "#{Blog.base_url}/blogs/#{resource_id}").
24
+ with(headers: { 'Accept' => 'application/json' }).
25
+ to_return(status: 200, body: resource_values.to_json)
26
+
27
+ result = Blog.find(resource_id)
28
+ expect(result).to be_a Blog
29
+ expect(result.attributes).to match (resource_values)
30
+ end
31
+ end
32
+
33
+ context '#all' do
34
+ it 'fetches all resourced' do
35
+ resource_values = [{ id: 1257, title: 'Hello' },
36
+ { id: 33, title: 'World' },
37
+ { id: 38, title: 'Bonjour!' }]
38
+
39
+ stub_request(:get, "#{Blog.base_url}/blogs").
40
+ with(headers: { 'Accept' => 'application/json' }).
41
+ to_return(status: 200, body: resource_values.to_json)
42
+
43
+ blogs = Blog.all
44
+
45
+ expect(blogs).to be_a Array
46
+ blogs.each { |e| expect(e).to be_a Blog }
47
+ expect(blogs.map { |e| e.attributes }).to match (resource_values)
48
+ end
49
+ end
50
+
51
+ context '#where' do
52
+ before do
53
+ @resource_values = [{ id: 1257, title: 'Hello' },
54
+ { id: 33, title: 'World' },
55
+ { id: 38, title: 'Bonjour!' }]
56
+ end
57
+ it 'fetches resourced filtered by query string with GET' do
58
+ stub_request(:get, "#{Blog.base_url}/blogs?title=hello+world&tags%5B%5D=hello&tags%5B%5D=first&tags%5B%5D=greeting").
59
+ with(headers: { 'Accept' => 'application/json' }).
60
+ to_return(status: 200, body: @resource_values.to_json)
61
+
62
+ blogs = Blog.where(title: 'hello world', tags: ['hello', 'first', 'greeting'])
63
+
64
+ expect(blogs).to be_a Array
65
+ blogs.each { |e| expect(e).to be_a Blog }
66
+ expect(blogs.map { |e| e.attributes }).to match (@resource_values)
67
+ end
68
+
69
+ it 'fetches resourced filtered by query string with POST' do
70
+ stub_request(:post, "#{Blog.base_url}/blogs").
71
+ with(headers: { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' },
72
+ body: 'title=hello%20world&tags[]=hello&tags[]=first&tags[]=greeting').
73
+ to_return(status: 200, body: @resource_values.to_json)
74
+
75
+ blogs = Blog.where({ title: 'hello world', tags: ['hello', 'first', 'greeting'] }, :post)
76
+
77
+ expect(blogs).to be_a Array
78
+ blogs.each { |e| expect(e).to be_a Blog }
79
+ expect(blogs.map { |e| e.attributes }).to match (@resource_values)
80
+ end
81
+ end
82
+
83
+ context '#by_parent_resource' do
84
+ it 'fetches resource nested with parent resource' do
85
+ resource_values = [{ id: 1257, title: 'Hello' },
86
+ { id: 33, title: 'World' }]
87
+ resource_id = 3
88
+
89
+ stub_request(:get, "#{Blog.base_url}/tags/#{resource_id}/blogs").
90
+ with(headers: { 'Accept' => 'application/json' }).
91
+ to_return(status: 200, body: resource_values.to_json)
92
+
93
+ blogs = Blog.by_tag(resource_id)
94
+
95
+ expect(blogs).to be_a Array
96
+ blogs.each { |e| expect(e).to be_a Blog }
97
+ expect(blogs.map { |e| e.attributes }).to match (resource_values)
98
+ end
99
+ end
100
+
101
+ context '#child_ressources' do
102
+ it 'has_many association with inlined json' do
103
+ resource_values = {
104
+ id: 1257, title: 'Tarte a la creme',
105
+ tags: [{ id: 33, name: 'food' }, { id: 39, name: 'cuisine' }]
106
+ }
107
+ resource_id = 3
108
+
109
+ stub_request(:get, "#{Blog.base_url}/blogs/#{resource_id}").
110
+ with(headers: { 'Accept' => 'application/json' }).
111
+ to_return(status: 200, body: resource_values.to_json)
112
+
113
+ blog = Blog.find(resource_id)
114
+ expect(blog).to be_a Blog
115
+ expect(blog.attributes).to match(resource_values.except :tags)
116
+
117
+ tags = blog.tags
118
+ expect(tags).to be_a Array
119
+ tags.each { |e| expect(e).to be_a Tag }
120
+ expect(tags.map { |e| e.attributes }).to match (resource_values[:tags])
121
+ end
122
+ it 'has_one association with inlined json' do
123
+ resource_values = {
124
+ id: 1257, title: 'Tarte a la creme',
125
+ category: { id: 33, name: 'Hobbies' }
126
+ }
127
+ resource_id = 3
128
+
129
+ stub_request(:get, "#{Blog.base_url}/blogs/#{resource_id}").
130
+ with(headers: { 'Accept' => 'application/json' }).
131
+ to_return(status: 200, body: resource_values.to_json)
132
+
133
+ blog = Blog.find(resource_id)
134
+ expect(blog).to be_a Blog
135
+ expect(blog.attributes).to match(resource_values.except :category)
136
+
137
+ category = blog.category
138
+ expect(category).to be_a Category
139
+ expect(category.attributes).to match(resource_values[:category])
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,88 @@
1
+ require 'rspec'
2
+ require 'api-resource'
3
+ require 'webmock/rspec'
4
+
5
+ # Conventionally, all specs live under a `spec` directory, which RSpec adds to
6
+ # the `$LOAD_PATH`. The generated `.rspec` file contains `--require spec_helper`
7
+ # which will cause this file to always be loaded, without a need to explicitly
8
+ # require it in any files.
9
+ #
10
+ # Given that it is always loaded, you are encouraged to keep this file as
11
+ # light-weight as possible. Requiring heavyweight dependencies from this file
12
+ # will add to the boot time of your test suite on EVERY test run, even for an
13
+ # individual file that may not need all of that loaded. Instead, consider making
14
+ # a separate helper file that requires the additional dependencies and performs
15
+ # the additional setup, and require it from the spec files that actually need it.
16
+ #
17
+ # The `.rspec` file also contains a few flags that are not defaults but that
18
+ # users commonly want.
19
+ #
20
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
21
+ RSpec.configure do |config|
22
+ # rspec-expectations config goes here. You can use an alternate
23
+ # assertion/expectation library such as wrong or the stdlib/minitest
24
+ # assertions if you prefer.
25
+ config.expect_with :rspec do |expectations|
26
+ # This option will default to `true` in RSpec 4. It makes the `description`
27
+ # and `failure_message` of custom matchers include text for helper methods
28
+ # defined using `chain`, e.g.:
29
+ # be_bigger_than(2).and_smaller_than(4).description
30
+ # # => "be bigger than 2 and smaller than 4"
31
+ # ...rather than:
32
+ # # => "be bigger than 2"
33
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
34
+ end
35
+
36
+ # rspec-mocks config goes here. You can use an alternate test double
37
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
38
+ config.mock_with :rspec do |mocks|
39
+ # Prevents you from mocking or stubbing a method that does not exist on
40
+ # a real object. This is generally recommended, and will default to
41
+ # `true` in RSpec 4.
42
+ mocks.verify_partial_doubles = true
43
+ end
44
+
45
+ # The settings below are suggested to provide a good initial experience
46
+ # with RSpec, but feel free to customize to your heart's content.
47
+
48
+ # These two settings work together to allow you to limit a spec run
49
+ # to individual examples or groups you care about by tagging them with
50
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
51
+ # get run.
52
+ config.filter_run :focus
53
+ config.run_all_when_everything_filtered = true
54
+
55
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
56
+ # For more details, see:
57
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
58
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
59
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
60
+ config.disable_monkey_patching!
61
+
62
+ # Many RSpec users commonly either run the entire suite or an individual
63
+ # file, and it's useful to allow more verbose output when running an
64
+ # individual spec file.
65
+ if config.files_to_run.one?
66
+ # Use the documentation formatter for detailed output,
67
+ # unless a formatter has already been configured
68
+ # (e.g. via a command-line flag).
69
+ config.default_formatter = 'doc'
70
+ end
71
+
72
+ # Print the 10 slowest examples and example groups at the
73
+ # end of the spec run, to help surface which specs are running
74
+ # particularly slow.
75
+ config.profile_examples = 10
76
+
77
+ # Run specs in random order to surface order dependencies. If you find an
78
+ # order dependency and want to debug it, you can fix the order by providing
79
+ # the seed, which is printed after each run.
80
+ # --seed 1234
81
+ config.order = :random
82
+
83
+ # Seed global randomization in this process using the `--seed` CLI option.
84
+ # Setting this allows you to use `--seed` to deterministically reproduce
85
+ # test failures related to randomization by passing the same `--seed` value
86
+ # as the one that triggered the failure.
87
+ Kernel.srand config.seed
88
+ end
metadata ADDED
@@ -0,0 +1,167 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api-resource
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chaker Nakhli
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: simple-hmac
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rest-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activemodel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.20'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.20'
125
+ description:
126
+ email:
127
+ - chaker@wizypay.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - LICENSE.txt
133
+ - README.md
134
+ - Rakefile
135
+ - lib/api-resource.rb
136
+ - lib/api-resource/resource.rb
137
+ - lib/api-resource/version.rb
138
+ - spec/resource_spec.rb
139
+ - spec/spec_helper.rb
140
+ homepage: http://www.wizypay.com
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options:
146
+ - "--charset=UTF-8"
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: 1.9.2
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.4.3
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: A lightweight REST API access library.
165
+ test_files:
166
+ - spec/resource_spec.rb
167
+ - spec/spec_helper.rb