tp_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +55 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +12 -0
- data/Gemfile +14 -0
- data/LICENSE +7 -0
- data/README.md +203 -0
- data/Rakefile +26 -0
- data/lib/tiny_client.rb +15 -0
- data/lib/tiny_client/base_error.rb +6 -0
- data/lib/tiny_client/configuration.rb +35 -0
- data/lib/tiny_client/curb_requestor.rb +85 -0
- data/lib/tiny_client/nested_support.rb +95 -0
- data/lib/tiny_client/pagination_support.rb +53 -0
- data/lib/tiny_client/remote_client.rb +66 -0
- data/lib/tiny_client/request_error.rb +7 -0
- data/lib/tiny_client/resource.rb +232 -0
- data/lib/tiny_client/resource_error.rb +7 -0
- data/lib/tiny_client/response.rb +73 -0
- data/lib/tiny_client/response_error.rb +13 -0
- data/lib/tiny_client/url_builder.rb +43 -0
- data/tiny_client.gemspec +33 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ab9aaf3befe004379de592b2c5013cf917eacf5e
|
4
|
+
data.tar.gz: 77c525ffff5cd8be2a655f921db1326fa39fa883
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5c6d7870119bd22e8387aef07b94a454fd33c956150ac07e7a9b0ca58fa7197e8544802f047954a54826d97543bf68dcd7d91422fbac0c2950a52502568fb0a7
|
7
|
+
data.tar.gz: 0f451d61b4b1d4314836781de07afc846ffec1eb45adf0a4ff24b173384e599741c938d15d5c448b89105a539c2d96975012872ddf4ac1fc8913d73e8de616d8
|
data/.gitignore
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/gems/
|
6
|
+
/InstalledFiles
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/spec/examples.txt
|
10
|
+
/test/tmp/
|
11
|
+
/test/version_tmp/
|
12
|
+
/tmp/
|
13
|
+
|
14
|
+
# Byebug
|
15
|
+
.byebug_history
|
16
|
+
|
17
|
+
# Used by dotenv library to load environment variables.
|
18
|
+
# .env
|
19
|
+
|
20
|
+
## Specific to RubyMotion:
|
21
|
+
.dat*
|
22
|
+
.repl_history
|
23
|
+
build/
|
24
|
+
*.bridgesupport
|
25
|
+
build-iPhoneOS/
|
26
|
+
build-iPhoneSimulator/
|
27
|
+
|
28
|
+
## Specific to RubyMotion (use of CocoaPods):
|
29
|
+
#
|
30
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
31
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
32
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
33
|
+
#
|
34
|
+
# vendor/Pods/
|
35
|
+
|
36
|
+
## Documentation cache and generated files:
|
37
|
+
/.yardoc/
|
38
|
+
/_yardoc/
|
39
|
+
/doc/
|
40
|
+
/rdoc/
|
41
|
+
|
42
|
+
## Environment normalization:
|
43
|
+
/.bundle/
|
44
|
+
/vendor/bundle
|
45
|
+
/lib/bundler/man/
|
46
|
+
|
47
|
+
# for a library or gem, you might want to ignore these files since the code is
|
48
|
+
# intended to run in multiple environments; otherwise, check them in:
|
49
|
+
Gemfile.lock
|
50
|
+
.ruby-version
|
51
|
+
.ruby-gemset
|
52
|
+
|
53
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
54
|
+
.rvmrc
|
55
|
+
*tags
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.4.0
|
4
|
+
deploy:
|
5
|
+
provider: rubygems
|
6
|
+
api_key:
|
7
|
+
secure: rhnpzPKF7/SYka8XSo8ohtIT/YZB+eltG+uK1Yrup+RHYUvvjL5VbGuVXK71ouNtA5g0iA0xHB8lYyXBkC/DsovURjwhYQXymykqWz68yGjCQgl3i0IP8bgntAP5+PYbeOvYpy8DlH+6qakoTewa/cOhYvLLVlE91zk92UFORIW2b0TVxxMASB2cuKBOkI5a7vRpncpBYupIWLMIHwQd2IGPlWCPOx53QumZkuG1Xze8wuxGTdT+Oo4pIJsNZi1Oz5KKbOaYgYOZa29TlXesz43HcTruIIeMJgKuVMOMgSO0OuQGZa2wK5r7frWqepL8fOvkxS1MYM/9CBC2oUDPQMfX8OIbKQrvgmmIWy5gb78PXQzc8nzYXi+D8HZSydL+7lGnW6Jj5wUUcZ/s8pzfUtC5vgVq+mA7HssKwt3vFACh2prDa+EZuas7jDz36ek9GZsKooKHzMaN2gO9um8JfwgonWbqvXJvhz64QOqTu8ySaWX4is0Y93wc+3fLHPtS6gnyG/ClhLtm7PtJ1+LPo6xrn+9dMfcMfpLTq/z1urHJyzj+8NspMp+ZAZVu+WgfmfcX3/UmzOUBcTOCjA4i2L+jEydFgrOJhYXgGp3iCueKUhHmAAFzWkG8Vtt/KgmvW400kjCAjqIKYo+2DZjlqILZbT19Kr/ZMqDiwnB5ozI=
|
8
|
+
gem: tp_client
|
9
|
+
gemspec: tiny_client.gemspec
|
10
|
+
on:
|
11
|
+
tags: true
|
12
|
+
repo: TINYhr/tiny_client
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2017 TINYpulse
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# TINYclient, a tiny HTTP/JSON crud client toolkit
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/tiny_client.svg)](https://badge.fury.io/rb/tiny_client) [![Build Status](https://travis-ci.org/TINYhr/tiny_client.svg)](https://travis-ci.org/TINYhr/tiny_client) [![Code Climate](https://codeclimate.com/github/TINYhr/tiny_client/badges/gpa.svg)](https://codeclimate.com/github/TINYhr/tiny_client)
|
3
|
+
|
4
|
+
TINYclient is inspired by [Active Record](http://guides.rubyonrails.org/active_record_basics.html) and based on [Curb](https://github.com/taf2/curb).
|
5
|
+
|
6
|
+
### Setup
|
7
|
+
|
8
|
+
* Be sure that Curb/Curl works properly on your machine.
|
9
|
+
* install the gem
|
10
|
+
|
11
|
+
```sh
|
12
|
+
gem install tiny_client
|
13
|
+
```
|
14
|
+
|
15
|
+
### Getting Started
|
16
|
+
|
17
|
+
|
18
|
+
#### Configuration
|
19
|
+
|
20
|
+
You can initialize your API by extending the `TinyClient::Configuration`
|
21
|
+
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class MyConf < TinyClient::Configuration
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@url = 'http://localhost:3000/api/1.0'
|
28
|
+
@headers = { 'Authorization' => 'token asdfasf4ffsafasdf@12rfsdfa' }
|
29
|
+
@limit = 100
|
30
|
+
@connect_timeout = 30 # seconds
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
```
|
35
|
+
|
36
|
+
You can use that configuration in your resource.
|
37
|
+
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Author < TinyClient::Resource
|
41
|
+
conf MyConf.instance
|
42
|
+
|
43
|
+
path 'authors' # query will be made on http://localhost:3000/api/1.0/authors
|
44
|
+
|
45
|
+
fields :id, :name # your resource attributes
|
46
|
+
|
47
|
+
nested Books # your resource nested resource
|
48
|
+
end
|
49
|
+
|
50
|
+
class Book < TinyClient::Resource
|
51
|
+
conf MyConf.instance
|
52
|
+
path 'books'
|
53
|
+
fields :id, :title
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
#### Usage
|
58
|
+
|
59
|
+
Now you will be able to this:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
author = Author.show(1) # Get /authors/1.json
|
63
|
+
author.name = 'P. K. D.'
|
64
|
+
author.save! # PUT /authors/1.json { "author" : { "name" : "Bob" } }
|
65
|
+
|
66
|
+
book = Book.new
|
67
|
+
book.title = 'Confessions of a crap artist'
|
68
|
+
book = author.add_book(book) # POST /authors/1/books.json { "book" : { "title" : ".." }
|
69
|
+
|
70
|
+
book.id.present? # true
|
71
|
+
|
72
|
+
books = Book.index(limit: 10) # GET /books.json?limit=10
|
73
|
+
|
74
|
+
ed = Author.new
|
75
|
+
ed.name = 'Poe'
|
76
|
+
ed.save! # POST /authors.json { "author" : { "name" : "Poe" } }
|
77
|
+
ed.id.present?
|
78
|
+
|
79
|
+
ed_books = ed.books(limit: 10) # GET /authors/{ed.id}/books.json
|
80
|
+
first = ed_books.first
|
81
|
+
first.load! # GET /books/{first.id}.json
|
82
|
+
first.name.present?
|
83
|
+
|
84
|
+
# You can also navigate through all resources
|
85
|
+
|
86
|
+
Author.index_all do |author| # It will retrieve all the authors, using limit, and offset query params to paginate
|
87
|
+
# Do something for each author
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
Author.index_in_batches(limit: 1000) do |authors|
|
92
|
+
# retrieve authors by batch of 1000
|
93
|
+
end
|
94
|
+
|
95
|
+
```
|
96
|
+
|
97
|
+
### Instance methods behavior
|
98
|
+
|
99
|
+
#### load!
|
100
|
+
|
101
|
+
It will perform a get request on the resource id and set the resource fields value to the value retrived by the response.
|
102
|
+
|
103
|
+
```
|
104
|
+
author.load! # GET /authors/{author.id}.json -> { id: 1, name: 'Toto' ... }
|
105
|
+
author.name # 'Toto'
|
106
|
+
```
|
107
|
+
|
108
|
+
#### save!
|
109
|
+
|
110
|
+
It will create the resource if `id` is not set, otherwise it will update it.
|
111
|
+
The resource fields value will be updated by the response body content.
|
112
|
+
|
113
|
+
```
|
114
|
+
toto = MyModule::Author.new
|
115
|
+
toto.save! # POST /authors.json { author: {} }
|
116
|
+
|
117
|
+
toto.id # should have been set by through the reponse
|
118
|
+
|
119
|
+
toto.name = 'Toto'
|
120
|
+
toto.save! # PUT {author: {name: 'Toto'}} -> /authors/{toto.id}.json
|
121
|
+
```
|
122
|
+
|
123
|
+
Only `changed` values will be passed through the body content.
|
124
|
+
|
125
|
+
You can `clear` changes with `#clear_changes!`
|
126
|
+
You can now which fields has been marked has changed with `#changes`
|
127
|
+
|
128
|
+
Changes is automatically clear when you perform a request ( i.e call, `#show #index #get #put #post save!` and so on)
|
129
|
+
|
130
|
+
### Nested resource
|
131
|
+
|
132
|
+
You can add a nested resource thanks to the `nested` class methods.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class Author < TinyClient::Resource
|
136
|
+
nested Books, Magazines
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
It will allows you to call your nested resource directly from an instance of your parent resource.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
author = Author.show(1)
|
144
|
+
author.books(limit: 100) # index GET /authors/1/books.json?limit=100
|
145
|
+
book = author.book(1) # show GET/authors/1/books/1.json
|
146
|
+
book.title = 'New title'
|
147
|
+
author.update_book(book) # update PUT /authors/1/books/1.json -- { 'book': { 'title': 'New title' } }
|
148
|
+
author.remove_book(book) # destroy DELETE /authors/1/books/1.json
|
149
|
+
author.add_book(book) # create POST /author/1/books.json -- { 'book': { 'title': 'New title' } }
|
150
|
+
author.books_all.each # x GET /authors/1/books.json?limit=.. -- Enumerator -- Retrieve ALL books using limit and offset to handle pagination
|
151
|
+
```
|
152
|
+
|
153
|
+
This is equivalent to the following:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
author = Author.show(1)
|
157
|
+
author.nested_index(Book, limit: 100)
|
158
|
+
book = author.nested_show(Book, 1)
|
159
|
+
book.title = 'New title'
|
160
|
+
author.nested_update(book)
|
161
|
+
author.nested_delete(book)
|
162
|
+
author.nested_create(book)
|
163
|
+
author.nested_all(Book, limit: 10) # retrieve all books, quering the server by batch of 10;
|
164
|
+
```
|
165
|
+
|
166
|
+
### Constraint & Support
|
167
|
+
|
168
|
+
#### JSON only
|
169
|
+
|
170
|
+
TinyClient supports only JSON data.
|
171
|
+
`Accept: application/json` header is set.
|
172
|
+
|
173
|
+
#### POST/PUT create/update
|
174
|
+
|
175
|
+
The content passed to the server will always be prefixed by the class name in lower case.
|
176
|
+
|
177
|
+
```
|
178
|
+
toto = MyModule::Author.new
|
179
|
+
toto.save! # POST { author: {} }
|
180
|
+
|
181
|
+
```
|
182
|
+
|
183
|
+
#### Pagination / Buffer
|
184
|
+
|
185
|
+
Pagination, buffer is achieve through `limit` and `offset` params.
|
186
|
+
|
187
|
+
```
|
188
|
+
Author.index_all({limit: 100}) # Will queries the server by batch of 100, until all authors has been retrieved through the enumerator.
|
189
|
+
|
190
|
+
```
|
191
|
+
|
192
|
+
#### Content-Encoding support
|
193
|
+
|
194
|
+
TinyClient support `gzip` Content-Encoding. Response with `gzip` Content-Encoding will be automatically decompressed.
|
195
|
+
You can set the `Accept-Encoding: gzip` through the configuration headers.
|
196
|
+
|
197
|
+
### Development
|
198
|
+
|
199
|
+
You can run the test using:
|
200
|
+
|
201
|
+
```shell
|
202
|
+
rake
|
203
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
task default: [:test]
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.test_files = FileList['test/tiny_client/*_test.rb', 'test/tiny_client_test.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
YARD::Rake::YardocTask.new do |t|
|
13
|
+
t.files = ['lib/**/*.rb']
|
14
|
+
t.options = ['-o docs']
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Build the gem'
|
18
|
+
task gem: [:test] do
|
19
|
+
sh 'gem build tiny_client.gemspec'
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Clean up'
|
23
|
+
task :clean do
|
24
|
+
sh 'rm -rf docs'
|
25
|
+
sh 'rm -f tiny_client*.gem'
|
26
|
+
end
|
data/lib/tiny_client.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'tiny_client/curb_requestor'
|
2
|
+
require 'tiny_client/response'
|
3
|
+
require 'tiny_client/nested_support'
|
4
|
+
require 'tiny_client/pagination_support'
|
5
|
+
require 'tiny_client/request_error'
|
6
|
+
require 'tiny_client/response_error'
|
7
|
+
require 'tiny_client/resource_error'
|
8
|
+
require 'tiny_client/resource'
|
9
|
+
require 'tiny_client/url_builder'
|
10
|
+
require 'tiny_client/configuration'
|
11
|
+
require 'tiny_client/remote_client'
|
12
|
+
|
13
|
+
# Placeholder
|
14
|
+
module TinyClient
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module TinyClient
|
2
|
+
# Provides the default client configuration
|
3
|
+
# Subclass and override {#initialize} to implement a client confiuration.
|
4
|
+
# @abstract
|
5
|
+
# @attr_reader [String] url the api root url (i.e: http://localhost/api/1.0)
|
6
|
+
# @attr_reader [Integer] limit default limit used as a query param
|
7
|
+
class Configuration
|
8
|
+
include Singleton
|
9
|
+
attr_reader :url, :limit
|
10
|
+
|
11
|
+
# You need to initialize the api {#url}, default {#headers}, and default limit.
|
12
|
+
def initialize
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Integer] request connection timeout in seconds
|
17
|
+
def connect_timeout
|
18
|
+
@connect_timeout ||= 30
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Hash] headers default headers you want to pass along every request
|
22
|
+
def headers
|
23
|
+
@headers ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Boolean] true if curl verbose option is set
|
27
|
+
def verbose
|
28
|
+
@verbose ||= false
|
29
|
+
end
|
30
|
+
|
31
|
+
def requestor
|
32
|
+
@requestor ||= TinyClient::RemoteClient.new(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'curb'
|
2
|
+
|
3
|
+
module TinyClient
|
4
|
+
# Allows to perform request with Curb and wrapped the response.
|
5
|
+
# Curb client are attached to a current thread Fiber. ( One curb per Fiber. )
|
6
|
+
module CurbRequestor
|
7
|
+
class << self
|
8
|
+
# Perform a get request with Curl
|
9
|
+
# @param [String] url the full url
|
10
|
+
# @param [Hash] headers the request headers
|
11
|
+
# @param [Integer] connect_timeout timeout if the request connection go over (in second)
|
12
|
+
# @param [Boolean] verbose set curl verbose mode
|
13
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
14
|
+
# @return [TinyClient::Response] the request response
|
15
|
+
def perform_get(url, headers, connect_timeout, verbose)
|
16
|
+
perform(:GET, url, nil, nil, headers: headers, connect_timeout: connect_timeout,
|
17
|
+
verbose: verbose)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Perform a put request with Curl
|
21
|
+
# @param [String] url the full url
|
22
|
+
# @param [Hash] headers the request headers
|
23
|
+
# @param [String] content the request body content
|
24
|
+
# @param [Integer] connect_timeout timeout if the request connection go over (in second)
|
25
|
+
# @param [Boolean] verbose set curl verbose mode
|
26
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
27
|
+
# @return [TinyClient::Response] the request response
|
28
|
+
def perform_put(url, headers, content, connect_timeout, verbose)
|
29
|
+
perform(:PUT, url, nil, content, headers: headers, connect_timeout: connect_timeout,
|
30
|
+
verbose: verbose)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Perform a post request with Curl
|
34
|
+
# @param [String] url the full url
|
35
|
+
# @param [Hash] headers the request headers
|
36
|
+
# @param [String] content the request body content
|
37
|
+
# @param [Integer] connect_timeout timeout if the request connection go over (in second)
|
38
|
+
# @param [Boolean] verbose set curl verbose mode
|
39
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
40
|
+
# @return [TinyClient::Response] the request response
|
41
|
+
def perform_post(url, headers, content, connect_timeout, verbose)
|
42
|
+
perform(:POST, url, content, nil, headers: headers, connect_timeout: connect_timeout,
|
43
|
+
verbose: verbose)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Perform a delete request with Curl
|
47
|
+
# @param [String] url the full url
|
48
|
+
# @param [Hash] headers the request headers
|
49
|
+
# @param [Integer] connect_timeout timeout if the request connection go over (in second)
|
50
|
+
# @param [Boolean] verbose set curl verbose mode
|
51
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
52
|
+
# @return [TinyClient::Response] the request response
|
53
|
+
def perform_delete(url, headers, connect_timeout, verbose)
|
54
|
+
perform(:DELETE, url, nil, nil, headers: headers, connect_timeout: connect_timeout,
|
55
|
+
verbose: verbose)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Perform a head request with Curl
|
59
|
+
# @param [String] url the full url
|
60
|
+
# @param [Hash] headers the request headers
|
61
|
+
# @param [Integer] connect_timeout timeout if the request connection go over (in second)
|
62
|
+
# @param [Boolean] verbose set curl verbose mode
|
63
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
64
|
+
# @return [TinyClient::Response] the request response
|
65
|
+
def perform_head(url, headers, connect_timeout, verbose)
|
66
|
+
perform(:HEAD, url, nil, nil, headers: headers, connect_timeout: connect_timeout,
|
67
|
+
verbose: verbose)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def perform(verb, url, post_body, put_data, options = {})
|
73
|
+
response = Response.new(Curl.http(verb, url, post_body, put_data) do |c|
|
74
|
+
c.headers = options[:headers]
|
75
|
+
c.connect_timeout = options[:connect_timeout]
|
76
|
+
c.verbose = options[:verbose]
|
77
|
+
end)
|
78
|
+
raise ResponseError, response if response.error?
|
79
|
+
response
|
80
|
+
rescue Curl::Err::ConnectionFailedError => e
|
81
|
+
raise RequestError, e.message
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module TinyClient
|
4
|
+
#
|
5
|
+
# Mixin that add support for nested resource to {TinyClient::Resource}
|
6
|
+
# Each nested resource will be accessible with:
|
7
|
+
# <resource_name>s # List the existing ( index )
|
8
|
+
# <resource_name>(id) # Show an existing ( show )
|
9
|
+
# add_<resource_name>(resource) # To create a new one ( post )
|
10
|
+
# remove_<resource_name>(resource) # Remove an existing ( delete )
|
11
|
+
# update_<resource_name>(resource) # Update an existing ( put )
|
12
|
+
# @see file:README.md#label-Nested+resource README - Nested Resource
|
13
|
+
module NestedSupport
|
14
|
+
# @raise [ArgumentError] if the given resource_class is not a Resource
|
15
|
+
def self.included(resource_class)
|
16
|
+
raise ArgumentError, 'Works only for TinyClient::Resource' unless resource_class <= Resource
|
17
|
+
resource_class.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @raise [ArgumentError] if the given resource_class is not a Resource
|
21
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
22
|
+
def nested_show(resource_class, id, params = {})
|
23
|
+
raise ArgumentError, 'Works only for TinyClient::Resource' unless resource_class <= Resource
|
24
|
+
path = UrlBuilder.url(resource_class.path).path(id).build!
|
25
|
+
self.class.get(params, @id, path, resource_class)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @raise [ArgumentError] if the given resource_class is not a Resource
|
29
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
30
|
+
def nested_index(resource_class, params = {})
|
31
|
+
raise ArgumentError, 'Works only for TinyClient::Resource' unless resource_class <= Resource
|
32
|
+
self.class.get(params, @id, resource_class.path, resource_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @raise [ArgumentError] if the given resource does not have an id or is not Resource instance
|
36
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
37
|
+
def nested_update(resource)
|
38
|
+
raise ArgumentError, 'resource must be an TinyClient::Resource' unless resource.is_a? Resource
|
39
|
+
raise ArgumentError, 'resource must have id set' if resource.id.nil?
|
40
|
+
path = UrlBuilder.url(resource.class.path).path(resource.id).build!
|
41
|
+
data = resource.changes.to_a.each_with_object({}) { |fld, h| h[fld] = resource.send(fld) }
|
42
|
+
self.class.put({ resource.class.low_name => data }, @id, path, resource.class)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @raise [ArgumentError] if the given resource does not have an id or is not a Resource instance
|
46
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
47
|
+
def nested_delete(resource)
|
48
|
+
raise ArgumentError, 'resource must be an TinyClient::Resource' unless resource.is_a? Resource
|
49
|
+
raise ArgumentError, 'resource must have id set' if resource.id.nil?
|
50
|
+
path = UrlBuilder.url(resource.class.path).path(resource.id).build!
|
51
|
+
self.class.delete(@id, path, resource.class)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
55
|
+
def nested_create(resource)
|
56
|
+
raise ArgumentError, 'resource must be an TinyClient::Resource' unless resource.is_a? Resource
|
57
|
+
data = resource.changes.to_a.each_with_object({}) { |fld, h| h[fld] = resource.send(fld) }
|
58
|
+
self.class.post({ resource.class.low_name => data }, @id, resource.class.path, resource.class)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @see PaginationSupport::ClassMethods.get_all
|
62
|
+
# @raise [ArgumentError] if the given resource_class is not a Resource
|
63
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
64
|
+
def nested_all(resource_class, params = {})
|
65
|
+
raise ArgumentError, 'Works only for TinyClient::Resource' unless resource_class <= Resource
|
66
|
+
self.class.get_all(params, @id, resource_class.path, resource_class)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add support for the {#nested} class methods as well as default actions.
|
70
|
+
module ClassMethods
|
71
|
+
# Set nested resources. Nested resource creation and getters method will be created.
|
72
|
+
# If the resource class is called Post, then `add_post` and `posts` methods will be created.
|
73
|
+
# @param [Resource] clazz the nested resource class.
|
74
|
+
def nested(*clazz)
|
75
|
+
@nested ||= nested_actions(clazz) && clazz
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def nested_actions(nested)
|
81
|
+
nested.each do |clazz|
|
82
|
+
plural_name = ActiveSupport::Inflector.pluralize(clazz.low_name)
|
83
|
+
class_eval <<-RUBY
|
84
|
+
def #{plural_name}(params = {}); nested_index(#{clazz}, params) end
|
85
|
+
def #{clazz.low_name}(id, params = {}); nested_show(#{clazz}, id, params) end
|
86
|
+
def add_#{clazz.low_name}(#{clazz.low_name}); nested_create(#{clazz.low_name}) end
|
87
|
+
def update_#{clazz.low_name}(#{clazz.low_name}); nested_update(#{clazz.low_name}) end
|
88
|
+
def remove_#{clazz.low_name}(#{clazz.low_name}); nested_delete(#{clazz.low_name}) end
|
89
|
+
def #{plural_name}_all(params = {}); nested_all(#{clazz}, params) end
|
90
|
+
RUBY
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TinyClient
|
2
|
+
# Mixin that add support for limit/offset pagination to {TinyClient::Resource}
|
3
|
+
module PaginationSupport
|
4
|
+
def self.included(resource_class)
|
5
|
+
raise ArgumentError, 'Works only for TinyClient::Resource' unless resource_class <= Resource
|
6
|
+
resource_class.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Add methods that allows to walk fully through collections thanks to limit/offset pagination.
|
10
|
+
# All methods return an enumerator that will query the server in batch based on the limit size
|
11
|
+
# and total number of items.
|
12
|
+
module ClassMethods
|
13
|
+
# Similar to {Resource.index} but return all resources available at this path.
|
14
|
+
# It use limit and offset
|
15
|
+
# params to retrieved all resources. ( buffered by the limit size)
|
16
|
+
def index_all(params = {})
|
17
|
+
get_all(params)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Similar to {index_all}, the return enumerator will yield on the buffered ( limit )
|
21
|
+
# rather than each element.
|
22
|
+
def index_in_batches(params = {})
|
23
|
+
get_in_batches(params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_all(params = {}, id = nil, name = nil, resource_class = nil)
|
27
|
+
Enumerator.new do |y|
|
28
|
+
count = limit = params.fetch(:limit, @conf.limit || 100)
|
29
|
+
offset = params.fetch(:offset, 0)
|
30
|
+
while limit == count
|
31
|
+
inner = get(params.merge(limit: limit, offset: offset), id, name, resource_class)
|
32
|
+
loop { y << inner.next }
|
33
|
+
offset += limit
|
34
|
+
count = inner.count
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_in_batches(params = {}, id = nil, name = nil, resource_class = nil)
|
40
|
+
Enumerator.new do |y|
|
41
|
+
count = limit = params.fetch(:limit, @conf.limit || 100)
|
42
|
+
offset = params.fetch(:offset, 0)
|
43
|
+
while limit == count
|
44
|
+
inner = get(params.merge(limit: limit, offset: offset), id, name, resource_class)
|
45
|
+
loop { y << inner }
|
46
|
+
offset += limit
|
47
|
+
count = inner.count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module TinyClient
|
2
|
+
# Remote Http client which delegates to the {CurbRequestor}.
|
3
|
+
class RemoteClient
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
end
|
7
|
+
|
8
|
+
# GET /<path>/<id>/<name>?<params>
|
9
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
10
|
+
# @return [Response]
|
11
|
+
def get(path, params, id, name)
|
12
|
+
url = build_url(path, id, name).query(params).build!
|
13
|
+
CurbRequestor.perform_get(url, {
|
14
|
+
'Accept' => 'application/json',
|
15
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
16
|
+
}.merge!(@config.headers), @config.connect_timeout, @config.verbose)
|
17
|
+
end
|
18
|
+
|
19
|
+
# POST /<path>/<id>/<name>
|
20
|
+
# @param [Hash] data
|
21
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
22
|
+
# @return [Response]
|
23
|
+
def post(data, path, id, name)
|
24
|
+
url = build_url(path, id, name).build!
|
25
|
+
verify_json(data)
|
26
|
+
CurbRequestor.perform_post(url, {
|
27
|
+
'Accept' => 'application/json',
|
28
|
+
'Content-Type' => 'application/json'
|
29
|
+
}.merge!(@config.headers), data.to_json, @config.connect_timeout, @config.verbose)
|
30
|
+
end
|
31
|
+
|
32
|
+
# PUT /<path>/<id>/<name>
|
33
|
+
# @param [Hash] data the resource data
|
34
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
35
|
+
# @return [Response]
|
36
|
+
def put(data, path, id, name)
|
37
|
+
url = build_url(path, id, name).build!
|
38
|
+
verify_json(data)
|
39
|
+
CurbRequestor.perform_put(url, {
|
40
|
+
'Accept' => 'application/json',
|
41
|
+
'Content-Type' => 'application/json'
|
42
|
+
}.merge!(@config.headers), data.to_json, @config.connect_timeout, @config.verbose)
|
43
|
+
end
|
44
|
+
|
45
|
+
# DELETE /<path>/<id>.json
|
46
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
47
|
+
# @return [Response]
|
48
|
+
def delete(path, id, name)
|
49
|
+
url = build_url(path, id, name).build!
|
50
|
+
CurbRequestor.perform_delete(url, {
|
51
|
+
'Accept' => 'application/json',
|
52
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
53
|
+
}.merge!(@config.headers), @config.connect_timeout, @config.verbose)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def verify_json(data)
|
59
|
+
raise ArgumentError, 'data must respond to .to_json' unless data.respond_to? :to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_url(path, id, name)
|
63
|
+
UrlBuilder.url(@config.url).path(path).path(id).path(name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'set'
|
3
|
+
require 'active_support/json'
|
4
|
+
require 'active_support/core_ext/object/json'
|
5
|
+
module TinyClient
|
6
|
+
# This is the core of TinyClient.
|
7
|
+
# Subclass {TinyClient::Resource} in order to create an HTTP/JSON tiny client.
|
8
|
+
#
|
9
|
+
# {file:README.md Getting Started}
|
10
|
+
# @author @barjo
|
11
|
+
class Resource
|
12
|
+
include PaginationSupport
|
13
|
+
include NestedSupport
|
14
|
+
|
15
|
+
# A resource always have an id
|
16
|
+
attr_accessor :id
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Set this resource client configuration
|
20
|
+
# @param [Configuration] config the api url and client default headers.
|
21
|
+
def conf(config)
|
22
|
+
@conf ||= config
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set the resource path, default is the class name in lower case.
|
26
|
+
# @param [String] path the resource path
|
27
|
+
def path(path = nil)
|
28
|
+
@path ||= path || low_name
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [*String] names the resource field names
|
32
|
+
def fields(*names)
|
33
|
+
@fields ||= field_accessor(names) && names
|
34
|
+
end
|
35
|
+
|
36
|
+
# GET /<path>.json
|
37
|
+
# @param [Hash] params query parameters
|
38
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
39
|
+
# @return [Enumerator] enumerate the resources available at this path.
|
40
|
+
def index(params = {})
|
41
|
+
get(params)
|
42
|
+
end
|
43
|
+
|
44
|
+
# POST /<resource_path>.json
|
45
|
+
# Create a new resource. The resource will be indexed by it's name.
|
46
|
+
# @param [Object] content the resource/attributes to be created.
|
47
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
48
|
+
# @return the created resource
|
49
|
+
def create(content)
|
50
|
+
data = { low_name => content }
|
51
|
+
post(data)
|
52
|
+
end
|
53
|
+
|
54
|
+
# GET /<resource_path>/{id}
|
55
|
+
# @param [String, Integer] id the resource id
|
56
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
57
|
+
# @return the resource available at that path
|
58
|
+
def show(id, params = {})
|
59
|
+
get(params, id)
|
60
|
+
end
|
61
|
+
|
62
|
+
# GET /<path>/{id}/<name>
|
63
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
64
|
+
def get(params = {}, id = nil, name = nil, resource_class = nil)
|
65
|
+
resp = @conf.requestor.get(@path, params, id, name)
|
66
|
+
(resource_class || self).from_response resp
|
67
|
+
end
|
68
|
+
|
69
|
+
# POST /<path>/{id}/<name>
|
70
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
71
|
+
# @raise [ArgumentError] if data cannot be serialized as a json string ( .to_json )
|
72
|
+
def post(data, id = nil, name = nil, resource_class = nil)
|
73
|
+
resp = @conf.requestor.post(data, @path, id, name)
|
74
|
+
(resource_class || self).from_response resp
|
75
|
+
end
|
76
|
+
|
77
|
+
# Will query PUT /<path>/{id}
|
78
|
+
# @param [String, Integer] id the id of the resource that needs to be updated
|
79
|
+
# @param [Object] content the updated attributes/fields/resource
|
80
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
81
|
+
# @return the updated resource
|
82
|
+
def update(id, content)
|
83
|
+
data = { low_name => content }
|
84
|
+
put(data, id)
|
85
|
+
end
|
86
|
+
|
87
|
+
# PUT /<path>/{id}/<name>
|
88
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
89
|
+
# @raise [ArgumentError] if data cannot be serialized as a json string ( .to_json )
|
90
|
+
def put(data, id = nil, name = nil, resource_class = nil)
|
91
|
+
resp = @conf.requestor.put(data, @path, id, name)
|
92
|
+
(resource_class || self).from_response resp
|
93
|
+
end
|
94
|
+
|
95
|
+
# delete /<path>/{id}.json
|
96
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
97
|
+
def delete(id = nil, name = nil, resource_class = nil)
|
98
|
+
resp = @conf.requestor.delete(@path, id, name)
|
99
|
+
(resource_class || self).from_response resp
|
100
|
+
end
|
101
|
+
|
102
|
+
def low_name
|
103
|
+
@low_name ||= name.demodulize.underscore
|
104
|
+
end
|
105
|
+
|
106
|
+
# Create a resouce instance from an Hash.
|
107
|
+
# @param [Hash] hash the resource fields with their values
|
108
|
+
# @param [Boolean] track_changes if true all fields will be marked has changed
|
109
|
+
# @return [Resource] the newly created resource
|
110
|
+
def build(hash, track_changes = true)
|
111
|
+
resource = fields.each_with_object(new) do |field, r|
|
112
|
+
value = hash.fetch(field.to_s, hash[field.to_sym])
|
113
|
+
r.send("#{field}=", value)
|
114
|
+
end
|
115
|
+
resource.clear_changes! unless track_changes
|
116
|
+
resource
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Response] the last response that has been received for that resource
|
120
|
+
def last_response
|
121
|
+
Thread.current[:_tclr]
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
# Create a resource instance from a {Response}.
|
127
|
+
# If the response contains an Array of resource hash, an Enumerator will be return.
|
128
|
+
# @param [Response] response obtained from making a request.
|
129
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
130
|
+
# @return [Resource, Enumerator, nil] the resources created from the given response.
|
131
|
+
def from_response(response)
|
132
|
+
Thread.current[:_tclr] = response
|
133
|
+
case body = response.parse_body
|
134
|
+
when Hash
|
135
|
+
build(body, false)
|
136
|
+
when Array
|
137
|
+
Enumerator.new(body.size) do |yielder|
|
138
|
+
inner = body.each
|
139
|
+
loop { yielder << build(inner.next, false) }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def field_accessor(names)
|
147
|
+
names.each do |name|
|
148
|
+
class_eval <<-RUBY
|
149
|
+
def #{name}; @#{name} end
|
150
|
+
|
151
|
+
def #{name}=(#{name})
|
152
|
+
@#{name}= #{name}
|
153
|
+
@changes << :#{name} # keep track of fields that has been modified
|
154
|
+
end
|
155
|
+
RUBY
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# the fields that has beem modified, and will be save on {save!}
|
161
|
+
attr_reader :changes
|
162
|
+
|
163
|
+
def initialize(*_args)
|
164
|
+
@changes = Set.new # store the fields change here
|
165
|
+
end
|
166
|
+
|
167
|
+
# Save the resource fields that has changed, or create it, if it's a new one!
|
168
|
+
# Create the a new resource if id is not set or update the corresonding resource.
|
169
|
+
# Create is done by calling POST on the resource path
|
170
|
+
# Update is done by calling PUT on the resource id ( path/id )
|
171
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
172
|
+
# @return [Resource] the updated resource
|
173
|
+
def save!
|
174
|
+
data = @changes.to_a.each_with_object({}) { |field, h| h[field] = send(field) }
|
175
|
+
saved = id.present? ? self.class.update(id, data) : self.class.create(data)
|
176
|
+
clone_fields(saved)
|
177
|
+
clear_changes!
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
# Destroy this resource. It will call delete on this resource id.
|
182
|
+
# DELETE /path/id
|
183
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
184
|
+
# @raise [ResourceError] if this resource does not have an id.
|
185
|
+
# @return the deleted resource
|
186
|
+
def destroy!
|
187
|
+
raise ResourceError, 'Cannot delete resource if @id not present' if id.blank?
|
188
|
+
self.class.delete(id)
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
# Load/Reload this resource from the server.
|
193
|
+
# It will reset all fields that has been retrieved through the request.
|
194
|
+
# It will do a GET request on the resource id (:show)
|
195
|
+
# @param [Hash] params optional query parameters
|
196
|
+
# @raise [ResponseError] if the server respond with an error status (i.e 404, 500..)
|
197
|
+
# @raise [ResourceError] if this resource does not have an id.
|
198
|
+
# @return self with updated fields.
|
199
|
+
def load!(params = {})
|
200
|
+
raise ResourceError, 'Cannot load resource if @id not present' if id.blank?
|
201
|
+
# get the values from the persistence layer
|
202
|
+
reloaded = self.class.show(@id, params)
|
203
|
+
clone_fields(reloaded)
|
204
|
+
clear_changes!
|
205
|
+
reloaded
|
206
|
+
end
|
207
|
+
|
208
|
+
# Mark all fields has not changed. This mean that calling save! will not modify this resource
|
209
|
+
# until a field attribute has been changed.
|
210
|
+
def clear_changes!
|
211
|
+
@changes.clear
|
212
|
+
end
|
213
|
+
|
214
|
+
# see http://edgeguides.rubyonrails.org/active_support_core_extensions.html#json-support
|
215
|
+
# @param [Hash] options for the hash transformation
|
216
|
+
# @option [Array] only limit the hash content to those fields
|
217
|
+
# @return [Hash] a json ready representation of this resource
|
218
|
+
def as_json(options = {})
|
219
|
+
self.class.fields.each_with_object({}) do |field, h|
|
220
|
+
h[field] = send(field)
|
221
|
+
end.as_json(options)
|
222
|
+
end
|
223
|
+
|
224
|
+
alias to_h as_json
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def clone_fields(resource)
|
229
|
+
self.class.fields.each { |f| send("#{f}=", resource.send(f)) }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'active_support/json/decoding'
|
2
|
+
require 'active_support/gzip'
|
3
|
+
|
4
|
+
module TinyClient
|
5
|
+
# Wrap the curl request response.
|
6
|
+
class Response
|
7
|
+
attr_reader :status, :body_str, :header_str, :url, :code
|
8
|
+
|
9
|
+
def initialize(curb)
|
10
|
+
@status = curb.status
|
11
|
+
@body_str = curb.body_str
|
12
|
+
@header_str = curb.header_str
|
13
|
+
@code = @status.to_i
|
14
|
+
@url = curb.url
|
15
|
+
end
|
16
|
+
|
17
|
+
# Convert the response json body into an hash.
|
18
|
+
# @return the parsed response body
|
19
|
+
def parse_body
|
20
|
+
body = gzip? ? gzip_decompress : body_str
|
21
|
+
ActiveSupport::JSON.decode(body) if body.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse the X-Total-Count header
|
25
|
+
# @return [Integer] the value of the X-Total-Count header, or nil if not present
|
26
|
+
def total_count
|
27
|
+
count = header_str[/X-Total-Count: ([0-9]+)/, 1]
|
28
|
+
count.present? ? count.to_i : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return true if this response Content-Encoding is gzip
|
32
|
+
def gzip?
|
33
|
+
/Content-Encoding: gzip/ =~ header_str
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return true if the http request has been successful.
|
37
|
+
def success?
|
38
|
+
(200..299).cover?(@code)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return true if the HTTP status code of this response correspond to an client or server error.
|
42
|
+
def error?
|
43
|
+
@code >= 400
|
44
|
+
end
|
45
|
+
|
46
|
+
def client_error?
|
47
|
+
(400..499).cover?(@code)
|
48
|
+
end
|
49
|
+
|
50
|
+
def server_error?
|
51
|
+
@code >= 500
|
52
|
+
end
|
53
|
+
|
54
|
+
def redirect?
|
55
|
+
(300..399).cover?(@code)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
{
|
60
|
+
url: url,
|
61
|
+
status: status,
|
62
|
+
body: body_str,
|
63
|
+
headers: header_str
|
64
|
+
}.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def gzip_decompress
|
70
|
+
ActiveSupport::Gzip.decompress(body_str)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'tiny_client/base_error'
|
2
|
+
|
3
|
+
module TinyClient
|
4
|
+
# Raised when an HTTP error occured during the request. See {Response#error?}
|
5
|
+
class ResponseError < BaseError
|
6
|
+
attr_reader :response
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
@response = response
|
10
|
+
@message = "Error #{response.status} occured when calling #{response.url}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
|
3
|
+
module TinyClient
|
4
|
+
# Convenient class used to build a request URL.
|
5
|
+
class UrlBuilder
|
6
|
+
SEPARATOR = '/'.freeze
|
7
|
+
attr_writer :query
|
8
|
+
|
9
|
+
def self.url(url)
|
10
|
+
new(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(path)
|
14
|
+
@path << fix_path(path) unless path.blank?
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def query(params = {})
|
19
|
+
@query.merge!(params) unless params.empty?
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def build!
|
24
|
+
query_s = "?#{@query.to_query}" unless @query.empty?
|
25
|
+
"#{@path.join(SEPARATOR)}.json#{query_s}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def initialize(url)
|
31
|
+
@path = [url]
|
32
|
+
@query = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def fix_path(path)
|
36
|
+
if path.respond_to?(:gsub)
|
37
|
+
path.gsub(/\.json$/, '')
|
38
|
+
else
|
39
|
+
path
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/tiny_client.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'tp_client'
|
3
|
+
s.authors = ['TINYpulse Devops']
|
4
|
+
s.version = '0.1.0'
|
5
|
+
s.description = 'TINYclient, an HTTP/JSON crud client toolkit.'
|
6
|
+
s.email = 'devops@tinypulse.com'
|
7
|
+
s.extra_rdoc_files = ['LICENSE', 'README.md']
|
8
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
9
|
+
|
10
|
+
if s.respond_to?(:metadata)
|
11
|
+
s.metadata["allowed_push_host"] = 'https://rubygems.org'
|
12
|
+
else
|
13
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
14
|
+
"public gem pushes."
|
15
|
+
end
|
16
|
+
#### Load-time details
|
17
|
+
s.require_paths = %w(lib ext)
|
18
|
+
s.rubyforge_project = 'tiny_client'
|
19
|
+
s.summary = 'TINYclient is an HTTP/JSON crud toolkit inspired by ActiveRecord and based on Curb.'
|
20
|
+
s.test_files = ['test/tiny_client/']
|
21
|
+
|
22
|
+
#### Documentation and testing.
|
23
|
+
s.has_rdoc = 'yard'
|
24
|
+
s.homepage = 'https://github.com/TINYhr/tiny_client'
|
25
|
+
s.rdoc_options = ['--main', 'README.md']
|
26
|
+
|
27
|
+
s.platform = Gem::Platform::RUBY
|
28
|
+
|
29
|
+
s.license = 'MIT'
|
30
|
+
|
31
|
+
s.add_runtime_dependency 'curb', '> 0.7.0', '< 1.0.0'
|
32
|
+
s.add_runtime_dependency 'activesupport', '>= 4.0', '< 6.0'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tp_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TINYpulse Devops
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curb
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.7.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.7.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '4.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '4.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '6.0'
|
53
|
+
description: TINYclient, an HTTP/JSON crud client toolkit.
|
54
|
+
email: devops@tinypulse.com
|
55
|
+
executables: []
|
56
|
+
extensions: []
|
57
|
+
extra_rdoc_files:
|
58
|
+
- LICENSE
|
59
|
+
- README.md
|
60
|
+
files:
|
61
|
+
- ".gitignore"
|
62
|
+
- ".rubocop.yml"
|
63
|
+
- ".travis.yml"
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- lib/tiny_client.rb
|
69
|
+
- lib/tiny_client/base_error.rb
|
70
|
+
- lib/tiny_client/configuration.rb
|
71
|
+
- lib/tiny_client/curb_requestor.rb
|
72
|
+
- lib/tiny_client/nested_support.rb
|
73
|
+
- lib/tiny_client/pagination_support.rb
|
74
|
+
- lib/tiny_client/remote_client.rb
|
75
|
+
- lib/tiny_client/request_error.rb
|
76
|
+
- lib/tiny_client/resource.rb
|
77
|
+
- lib/tiny_client/resource_error.rb
|
78
|
+
- lib/tiny_client/response.rb
|
79
|
+
- lib/tiny_client/response_error.rb
|
80
|
+
- lib/tiny_client/url_builder.rb
|
81
|
+
- tiny_client.gemspec
|
82
|
+
homepage: https://github.com/TINYhr/tiny_client
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata:
|
86
|
+
allowed_push_host: https://rubygems.org
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options:
|
89
|
+
- "--main"
|
90
|
+
- README.md
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
- ext
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project: tiny_client
|
106
|
+
rubygems_version: 2.6.8
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: TINYclient is an HTTP/JSON crud toolkit inspired by ActiveRecord and based
|
110
|
+
on Curb.
|
111
|
+
test_files: []
|