fit_api 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.
- 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: []
|