fit_api 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +6 -0
- data/LICENSE.md +22 -0
- data/README.md +309 -0
- data/Rakefile +1 -0
- data/fit_api.gemspec +17 -0
- data/lib/fit_api.rb +12 -0
- data/lib/fit_api/app.rb +9 -0
- data/lib/fit_api/controller.rb +48 -0
- data/lib/fit_api/router.rb +36 -0
- data/lib/fit_api/router/mapper.rb +125 -0
- data/lib/fit_api/router/params.rb +51 -0
- data/lib/fit_api/router/parser.rb +41 -0
- data/lib/fit_api/router/route.rb +74 -0
- data/lib/fit_api/version.rb +3 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 47c9ea656a0413aed2e76fdbdd586c4df827a71fde3346eb1829526cf2d5fe72
|
4
|
+
data.tar.gz: 3105d64111f3d3bdb9fb1fc7882afe62498a0e06b40a363d7380e9a5713dd1d4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f02633bd1e2ce741ab55dd3e959827de5323458eeb3614be57759578537a2b64a52f43f1b51be4f55acf5566a138d89aa3307939cb421611a1e6039a4f120995
|
7
|
+
data.tar.gz: 4362fa551cf76e494f5408912da387c0553bbe469d574cd85123c56e022f25ab5539c48348870391e32d4b4f764488cd25673158eb83434e2759c0d09bbf5fc3
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright © 2014-2017 Luca Guidi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
# fit-api
|
2
|
+
|
3
|
+
Lightweight framework for building JSON API's
|
4
|
+
|
5
|
+
## Introduction
|
6
|
+
|
7
|
+
fit-api is a 400 line dependency library based on Rack and inspired by Rails & Sinatra.
|
8
|
+
The goal of this library is to provide simplicity when developing an API with ruby.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Install the latest version from RubyGems
|
13
|
+
|
14
|
+
```
|
15
|
+
$ gem install fit_api
|
16
|
+
```
|
17
|
+
|
18
|
+
## Table of Contents
|
19
|
+
|
20
|
+
* [fit-api](#fit-api)
|
21
|
+
* [Table of Contents](#table-of-contents)
|
22
|
+
* [Usage](#usage)
|
23
|
+
* [Router](#router)
|
24
|
+
* [Resource/s](#resources)
|
25
|
+
* [Namespace](#namespace)
|
26
|
+
* [Controller](#controller)
|
27
|
+
* [Root](#root)
|
28
|
+
* [404](#customize-error-404-message)
|
29
|
+
* [Controllers](#controllers)
|
30
|
+
* [Request](#request)
|
31
|
+
* [Params](#params)
|
32
|
+
* [Headers](#request-headers)
|
33
|
+
* [Callbacks](#callbacks)
|
34
|
+
* [Rack Middlewares](#rack-middlewares)
|
35
|
+
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
This is a basic example showing how it works... you can check the demo app from this repository:
|
40
|
+
[fit-api-demo](/bermanya/fit-api-demo)
|
41
|
+
|
42
|
+
**my_app.rb**
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'fit_api'
|
46
|
+
|
47
|
+
require_relative 'routes'
|
48
|
+
require_relative 'app_controller'
|
49
|
+
|
50
|
+
Rack::Handler::WEBrick.run FitApi::App.new
|
51
|
+
```
|
52
|
+
|
53
|
+
**routes.rb**
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
FitApi::Router.define do
|
57
|
+
get '/:name', to: 'app#show'
|
58
|
+
|
59
|
+
root to: 'app#index'
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
**app_controller.rb**
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class AppController < FitApi::Controller
|
67
|
+
def index
|
68
|
+
json({ message: 'Hello world' })
|
69
|
+
end
|
70
|
+
|
71
|
+
def show
|
72
|
+
json({ message: "Welcome #{params.name}" })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
```bash
|
78
|
+
ruby my_app.rb
|
79
|
+
```
|
80
|
+
|
81
|
+
## Router
|
82
|
+
|
83
|
+
It recognizes URLs and invoke the controller's action... the DSL is pretty similar to Rails (obviously not to so powerful):
|
84
|
+
|
85
|
+
### HTTP methods:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
get '/test/:name', to: 'app#test_show'
|
89
|
+
post '/test', to: 'app#test_post'
|
90
|
+
put '/test', to: 'app#test_put'
|
91
|
+
delete '/test/:id', to: 'app#test_delete'
|
92
|
+
```
|
93
|
+
|
94
|
+
### Resources
|
95
|
+
|
96
|
+
**Nested:**
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
resources :users do
|
100
|
+
resource :avatar do
|
101
|
+
get :comments
|
102
|
+
post :add_comment
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
**Endpoints for users:**
|
108
|
+
|
109
|
+
| Method | Path | Controller & action |
|
110
|
+
|-------------|--------------------|-----------------------|
|
111
|
+
| **GET** | /users | users#index |
|
112
|
+
| **GET** | /users/:id | users#show |
|
113
|
+
| **POST** | /users | users#create |
|
114
|
+
| **PATCH** | /users/:id | users#update |
|
115
|
+
| **DELETE** | /users/:id | users#destroy |
|
116
|
+
|
117
|
+
**Endpoints for avatar:**
|
118
|
+
|
119
|
+
| Method | Path | Controller & action |
|
120
|
+
|-------------|------------------------------------|-----------------------|
|
121
|
+
| **GET** | /users/:user_id/avatar | avatar#show |
|
122
|
+
| **POST** | /users/:user_id/avatar | avatar#create |
|
123
|
+
| **PATCH** | /users/:user_id/avatar | avatar#update |
|
124
|
+
| **DELETE** | /users/:user_id/avatar | avatar#destroy |
|
125
|
+
| **GET** | /users/:user_id/avatar/comments | avatar#comments |
|
126
|
+
| **POST** | /users/:user_id/avatar/add_comment | avatar#add_comment |
|
127
|
+
|
128
|
+
-----
|
129
|
+
|
130
|
+
**Member & Collection:**
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
resources :contacts, only: %i(index) do
|
134
|
+
member do
|
135
|
+
post :add_activity
|
136
|
+
end
|
137
|
+
|
138
|
+
collection do
|
139
|
+
get :search
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
| Method | Path | Controller & action |
|
145
|
+
|-------------|----------------------------|-----------------------|
|
146
|
+
| **GET** | /contacts | contacts#index |
|
147
|
+
| **GET** | /contacts/search | contacts#search |
|
148
|
+
| **POST** | /contacts/:id/add_activity | contacts#add_activity |
|
149
|
+
|
150
|
+
-----
|
151
|
+
|
152
|
+
### Namespace
|
153
|
+
|
154
|
+
Only for paths
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
namespace :test do
|
158
|
+
get :hello_world
|
159
|
+
post :hello_world, action: :post_hello_world
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
| Method | Path | Controller & action |
|
164
|
+
|------------|--------------------|-----------------------|
|
165
|
+
| **GET** | /test/hello_world | test#hello_world |
|
166
|
+
| **POST** | /test/hello_world | test#post_hello_world |
|
167
|
+
|
168
|
+
-----
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
namespace '/hello/world', controller: :test do
|
172
|
+
get :test
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
| Method | Path | Controller & action |
|
177
|
+
|----------|-------------------|-----------------------|
|
178
|
+
| **GET** | /test/world/test | test#test |
|
179
|
+
|
180
|
+
-----
|
181
|
+
|
182
|
+
### Controller
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
controller :app do
|
186
|
+
get :another_action
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
| Method | Path | Controller & action |
|
191
|
+
|------------|-------------------|-----------------------|
|
192
|
+
| **GET** | /another_action | app#another_action |
|
193
|
+
|
194
|
+
-----
|
195
|
+
|
196
|
+
### Root
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
root to: 'app#index'
|
200
|
+
```
|
201
|
+
|
202
|
+
| Method | Path | Controller & action |
|
203
|
+
|----------|--------|-----------------------|
|
204
|
+
| **GET** | / | app#index |
|
205
|
+
|
206
|
+
-----
|
207
|
+
|
208
|
+
### Customize error 404 message
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
not_found 'app#error_404'
|
212
|
+
```
|
213
|
+
|
214
|
+
## Controllers
|
215
|
+
|
216
|
+
The library provides one father class `FitApi::Controller` that should be inherited from your controllers.
|
217
|
+
One limitation is the class name of your controller must end with "Controller", i.e: AppController, UsersController...
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class AppController < FitApi::Controller
|
221
|
+
def index
|
222
|
+
json 'hello world'
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_post
|
226
|
+
json params
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
You have the method `#json` available, basically, it sets the Response body.
|
232
|
+
|
233
|
+
### Request
|
234
|
+
|
235
|
+
You can access the Request object like this: `request`
|
236
|
+
|
237
|
+
### Params
|
238
|
+
|
239
|
+
Assuming the following requests:
|
240
|
+
|
241
|
+
**GET /users/:id?name=Berna&age=28&height=180**
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
params.id # 1
|
245
|
+
params.name # "Berna"
|
246
|
+
params[:age] # 28
|
247
|
+
params['height'] # 180
|
248
|
+
```
|
249
|
+
|
250
|
+
**POST /test**
|
251
|
+
|
252
|
+
With Params:
|
253
|
+
|
254
|
+
```bash
|
255
|
+
curl -i -X POST -d 'user[name]=Berna&user[age]=28' http://server:1337/test
|
256
|
+
```
|
257
|
+
|
258
|
+
With JSON:
|
259
|
+
|
260
|
+
```bash
|
261
|
+
curl -i -X POST -H "Content-Type: application/json" -d '{ "user": { "name": "Berna", "age": 28 } }' http://server:1337/test
|
262
|
+
```
|
263
|
+
|
264
|
+
Then we have the following data in our `params` object:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
params.user # > Params
|
268
|
+
params.user.name # "Berna"
|
269
|
+
params[:user][:age] # 28
|
270
|
+
```
|
271
|
+
|
272
|
+
### Request Headers
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
request.headers['Authorization']
|
276
|
+
```
|
277
|
+
|
278
|
+
### Response Headers
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
headers['Header-Key'] = 'Header Value'
|
282
|
+
```
|
283
|
+
|
284
|
+
### Callbacks
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
before_action *actions
|
288
|
+
after_action *actions, only: %i(index show)
|
289
|
+
```
|
290
|
+
|
291
|
+
## Rack Middlewares
|
292
|
+
|
293
|
+
You can set up any rack middleware you want, i.e:
|
294
|
+
|
295
|
+
**config.ru**
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
require 'fit_api'
|
299
|
+
|
300
|
+
use Rack::CommonLogger, Logger.new('log/app.log')
|
301
|
+
|
302
|
+
run FitApi::App.new
|
303
|
+
```
|
304
|
+
|
305
|
+
## TODO:
|
306
|
+
- [ ] Implement tests
|
307
|
+
- [ ] Allow websockets -> `FitApi::Controller#stream`
|
308
|
+
|
309
|
+
Any contribution would be appreciated =)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/fit_api.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path("../lib/fit_api/version.rb", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'fit_api'
|
5
|
+
s.summary = 'Lightweight framework for building APIs'
|
6
|
+
s.description = 'fit-api is a Rack based framework for building JSON APIs'
|
7
|
+
s.author = 'Bernardo Castro'
|
8
|
+
s.email = 'bernacas@gmail.com'
|
9
|
+
s.version = FitApi.version
|
10
|
+
s.date = Time.now.strftime("%Y-%m-%d")
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
14
|
+
s.required_ruby_version = '>= 2.2.0'
|
15
|
+
s.add_dependency 'rack', '~> 2.0'
|
16
|
+
s.add_dependency 'dry-inflector', '~> 0.1'
|
17
|
+
end
|
data/lib/fit_api.rb
ADDED
data/lib/fit_api/app.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module FitApi
|
2
|
+
class Controller
|
3
|
+
attr_accessor :response
|
4
|
+
attr_reader :request, :params, :headers
|
5
|
+
|
6
|
+
def initialize(request, params)
|
7
|
+
@request = request
|
8
|
+
@params = params
|
9
|
+
@headers = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def actions
|
14
|
+
@actions ||= Hash.new { |h,k| h[k] = [] }
|
15
|
+
end
|
16
|
+
|
17
|
+
%i(before after).each do |callback_type|
|
18
|
+
define_method "#{callback_type}_action" do |*methods|
|
19
|
+
only = methods.last.is_a?(Hash) ? methods.last[:only] : nil
|
20
|
+
methods.each do |method|
|
21
|
+
unless method.is_a?(Hash)
|
22
|
+
actions[callback_type] << { method: method, only: only }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_response_headers
|
30
|
+
response.add_header 'Content-Type', 'application/json'
|
31
|
+
response.add_header 'Date', Rack::Utils.rfc2822(Time.now)
|
32
|
+
|
33
|
+
headers.each &response.method(:add_header)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def json(hash, status: 200)
|
39
|
+
self.response =
|
40
|
+
Rack::Response.new(hash.to_json, status)
|
41
|
+
end
|
42
|
+
|
43
|
+
def halt(status = 400, error)
|
44
|
+
json(error, status: status)
|
45
|
+
raise Halt
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'fit_api/router/mapper'
|
2
|
+
|
3
|
+
module FitApi
|
4
|
+
module Router
|
5
|
+
def self.call(env)
|
6
|
+
method, path = env['REQUEST_METHOD'], env['PATH_INFO']
|
7
|
+
route = find method, path, path != '/'
|
8
|
+
|
9
|
+
return route.invoke(env) if route
|
10
|
+
|
11
|
+
res = path == '/' ? { message: 'fit-api is working!' } : { error: 'action not found' }
|
12
|
+
|
13
|
+
[ res[:error] ? 404 : 200, { 'Content-Type' => 'application/json'}, [ res.to_json ] ]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find(method, path, find_error = true)
|
17
|
+
routes = mapper.routes[method.downcase]
|
18
|
+
route = routes.find { |route| route.match? path }
|
19
|
+
|
20
|
+
return route if route
|
21
|
+
|
22
|
+
if find_error
|
23
|
+
not_found = find('get', '/404', false)
|
24
|
+
return not_found if not_found
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.define(&block)
|
29
|
+
mapper.instance_eval &block
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.mapper
|
33
|
+
@mapper ||= Mapper.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'fit_api/router/route'
|
2
|
+
|
3
|
+
module FitApi
|
4
|
+
module Router
|
5
|
+
class Mapper
|
6
|
+
attr_reader :routes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@routes = Hash.new { |h,k| h[k] = [] }
|
10
|
+
@namespaces = []
|
11
|
+
end
|
12
|
+
|
13
|
+
%w(get post put delete patch).each do |verb|
|
14
|
+
define_method verb do |path, options = {}|
|
15
|
+
options[:controller] ||= @controller
|
16
|
+
route = Route.new(verb, "#{@namespaces.join}#{fix_path(path)}", options)
|
17
|
+
@routes[verb] << route
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
%i(resource resources).each do |resource_type|
|
22
|
+
define_method resource_type do |resource, options = {}, &block|
|
23
|
+
set_resource resource_type, resource, options, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def root(to:)
|
28
|
+
get '', to: to
|
29
|
+
end
|
30
|
+
|
31
|
+
def not_found(to)
|
32
|
+
get '/404', to: to
|
33
|
+
end
|
34
|
+
|
35
|
+
def member(&block)
|
36
|
+
namespace '/:id', &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def collection(&block)
|
40
|
+
instance_eval &block
|
41
|
+
end
|
42
|
+
|
43
|
+
def namespace(path, options = {}, &block)
|
44
|
+
@namespaces << fix_path(path)
|
45
|
+
if controller = options[:controller] || path
|
46
|
+
previous, @controller = @controller, controller
|
47
|
+
end
|
48
|
+
instance_eval &block
|
49
|
+
@controller = previous if controller
|
50
|
+
@namespaces.pop
|
51
|
+
end
|
52
|
+
|
53
|
+
def controller(controller, &block)
|
54
|
+
@controller = controller
|
55
|
+
instance_eval &block
|
56
|
+
@controller = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_resource(type, resource, options, &block)
|
62
|
+
options[:only] ||= %i(index show create update destroy)
|
63
|
+
path = get_path(type, resource)
|
64
|
+
|
65
|
+
@parent = [ type, resource ]
|
66
|
+
@controller = options[:controller] || resource
|
67
|
+
|
68
|
+
namespace path, options do
|
69
|
+
set_actions type, resource, options[:only]
|
70
|
+
instance_eval &block if block
|
71
|
+
end
|
72
|
+
|
73
|
+
@parent, @controller = nil, nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_actions(type, resource, actions)
|
77
|
+
path = get_resource_path(type)
|
78
|
+
|
79
|
+
actions.delete(:index) if type == :resource
|
80
|
+
|
81
|
+
actions.each do |action|
|
82
|
+
case action
|
83
|
+
when :index
|
84
|
+
get '', to: "#{resource}#index"
|
85
|
+
when :create
|
86
|
+
post '', to: "#{resource}#create"
|
87
|
+
when :show
|
88
|
+
get path, to: "#{resource}#show"
|
89
|
+
when :update
|
90
|
+
patch path, to: "#{resource}#update"
|
91
|
+
when :destroy
|
92
|
+
delete path, to: "#{resource}#destroy"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_path(type, resource)
|
98
|
+
return "/:#{s(@parent.last)}_id/#{resource}" if type == :resources && parent_is?(:resources)
|
99
|
+
return "/:#{s(@parent.last)}_id/#{s(resource)}" if type == :resource && parent_is?(:resources)
|
100
|
+
return "/#{s(resource)}" if type == :resource && parent_is?(:resource)
|
101
|
+
return "/#{resource}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def parent_is?(type)
|
105
|
+
@parent && @parent.first == type
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_resource_path(type)
|
109
|
+
return type == :resources ? '/:id' : ''
|
110
|
+
end
|
111
|
+
|
112
|
+
def fix_path(path)
|
113
|
+
if path.is_a?(Symbol) || path[0] != '/' && path != ''
|
114
|
+
"/#{path}"
|
115
|
+
else
|
116
|
+
path
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def s(word)
|
121
|
+
FitApi.inflector.singularize(word)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module FitApi
|
2
|
+
module Router
|
3
|
+
class Params
|
4
|
+
def initialize(hash)
|
5
|
+
@hash = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_json
|
9
|
+
@hash.to_json
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
value = @hash[key.to_s]
|
14
|
+
|
15
|
+
if value.is_a?(Hash)
|
16
|
+
self.class.new(value)
|
17
|
+
else
|
18
|
+
value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def []=(key, value)
|
23
|
+
@hash[key.to_s] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method_sym, *arguments, &block)
|
27
|
+
if @hash.include? method_sym.to_s
|
28
|
+
send('[]', method_sym.to_s)
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def except(*blacklist)
|
35
|
+
Params.new(
|
36
|
+
{}.tap do |h|
|
37
|
+
(@hash.keys - blacklist.map(&:to_s)).each { |k| h[k] = @hash[k] }
|
38
|
+
end
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def permit(*whitelist)
|
43
|
+
Params.new(
|
44
|
+
{}.tap do |h|
|
45
|
+
(@hash.keys & whitelist.map(&:to_s)).each { |k| h[k] = @hash[k] }
|
46
|
+
end
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module FitApi
|
2
|
+
module Router
|
3
|
+
class Parser
|
4
|
+
attr_reader :params
|
5
|
+
|
6
|
+
def initialize(route, path)
|
7
|
+
@route = route
|
8
|
+
@path = path.gsub(/\/$/, '')
|
9
|
+
@match = false
|
10
|
+
@params = {}
|
11
|
+
|
12
|
+
parse
|
13
|
+
end
|
14
|
+
|
15
|
+
def match?
|
16
|
+
@match
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse
|
22
|
+
result = @path.scan(/^#{regexp}$/)
|
23
|
+
set_params(result.flatten) if result.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_params(result)
|
27
|
+
@match = true
|
28
|
+
params = @route.scan(/\/\:+(\w+)/).flatten
|
29
|
+
|
30
|
+
params.each_index do |i|
|
31
|
+
@params[params[i]] =
|
32
|
+
result[i].match(/^\d+$/) ? result[i].to_i : URI.decode(result[i])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def regexp
|
37
|
+
@route.gsub(/\:\w+/, '([^\/]*)')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'fit_api/router/parser'
|
2
|
+
require 'fit_api/router/params'
|
3
|
+
|
4
|
+
module FitApi
|
5
|
+
module Router
|
6
|
+
class Route
|
7
|
+
attr_reader :verb, :path, :controller, :action
|
8
|
+
|
9
|
+
def initialize(verb, path, options = {})
|
10
|
+
@verb = verb
|
11
|
+
@path = path
|
12
|
+
@controller = get_controller(options)
|
13
|
+
@action = get_action(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def invoke(env)
|
17
|
+
request = Request.new(env)
|
18
|
+
route_params = parse(request.path).params
|
19
|
+
json_params = JSON.parse(request.body.read) rescue {}
|
20
|
+
params = Params.new(request.params.merge(route_params).merge(json_params))
|
21
|
+
controller = Object.const_get("#{@controller.capitalize}Controller").new(request, params)
|
22
|
+
|
23
|
+
run! controller
|
24
|
+
rescue Halt
|
25
|
+
controller.response
|
26
|
+
end
|
27
|
+
|
28
|
+
def match?(path)
|
29
|
+
parse(path).match?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def get_controller(options)
|
35
|
+
return options[:controller] if options[:controller]
|
36
|
+
options[:to].split('#').first
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_action(options)
|
40
|
+
return options[:action] if options[:action]
|
41
|
+
return options[:to].split('#').last if options[:to]
|
42
|
+
path[/\w+$/]
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse(path)
|
46
|
+
Parser.new(@path, path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def run!(controller)
|
50
|
+
run_callbacks! controller, :before
|
51
|
+
controller.send action
|
52
|
+
run_callbacks! controller, :after
|
53
|
+
controller.response
|
54
|
+
ensure
|
55
|
+
controller.set_response_headers
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_callbacks!(controller, type)
|
59
|
+
controller.class.actions[type].each do |action|
|
60
|
+
if action[:only].nil? || action[:only].include?(@action.to_sym)
|
61
|
+
controller.send action[:method]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Request < Rack::Request
|
68
|
+
def headers
|
69
|
+
env.select { |k,v| k.start_with? 'HTTP_'}.
|
70
|
+
transform_keys { |k| k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-') }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fit_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bernardo Castro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-inflector
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
description: fit-api is a Rack based framework for building JSON APIs
|
42
|
+
email: bernacas@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- ".gitignore"
|
48
|
+
- Gemfile
|
49
|
+
- LICENSE.md
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- fit_api.gemspec
|
53
|
+
- lib/fit_api.rb
|
54
|
+
- lib/fit_api/app.rb
|
55
|
+
- lib/fit_api/controller.rb
|
56
|
+
- lib/fit_api/router.rb
|
57
|
+
- lib/fit_api/router/mapper.rb
|
58
|
+
- lib/fit_api/router/params.rb
|
59
|
+
- lib/fit_api/router/parser.rb
|
60
|
+
- lib/fit_api/router/route.rb
|
61
|
+
- lib/fit_api/version.rb
|
62
|
+
homepage:
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.2.0
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 2.7.6
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: Lightweight framework for building APIs
|
86
|
+
test_files: []
|