yodatra 0.3.5 → 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +125 -1
- data/Rakefile +7 -0
- data/lib/yodatra/models_controller.rb +66 -48
- data/lib/yodatra/version.rb +1 -1
- data/spec/active_record/connection_adapters/fake_adapter.rb +2 -2
- data/spec/data/model.rb +1 -0
- data/spec/unit/models_controller_spec.rb +22 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6ce8fae3026a73118bcc2345e60dc4e1e93eabc
|
4
|
+
data.tar.gz: d6f0c9ac4382981d4fd48d8baec3864ed3018f0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ee95b6a0f7cf1b024c9a07a55e4babcc2381c1d408be1d4cfa2a51250ab9aab93dcafaf4241587428c99f231f2168617e9c50001b701e09061a42168a54bab2
|
7
|
+
data.tar.gz: a593575a605d5d1f134ad7a4c72b219b73d46e946a2957132a82d4a245b133c7aa183feb1fb72b59c608fd2aa457deb55292a1478d2b5be346e15a89f6e6552e
|
data/README.md
CHANGED
@@ -6,4 +6,128 @@ Backend development you shall do. And yodatra you shall use.
|
|
6
6
|
|
7
7
|
A minimalistic framework built on top of Sinatra it is.
|
8
8
|
|
9
|
-
|
9
|
+
The power of __ActiveRecord__ it gives you and the simplicity of a __Sinatra__ app. And all sort of small helpers.
|
10
|
+
|
11
|
+
## Instantly deploy your API
|
12
|
+
|
13
|
+
Based on your ActiveRecord models an API will be exposed very simply.
|
14
|
+
For every resource you want to expose, you will need to create a controller that inherits from the ```Yodatra::ModelsController```.
|
15
|
+
|
16
|
+
For example, given a `User` model
|
17
|
+
```ruby
|
18
|
+
class User < ActiveRecord::Base
|
19
|
+
# Your model definition
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
Creating a controller as simple as
|
24
|
+
```ruby
|
25
|
+
class UsersController < Yodatra::ModelsController
|
26
|
+
# limit read_scope
|
27
|
+
def read_scope
|
28
|
+
{ only: [:id, :name] }
|
29
|
+
end
|
30
|
+
|
31
|
+
# whitelist assignable attributes
|
32
|
+
def user_params
|
33
|
+
params.permit(:name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
will expose all these routes:
|
38
|
+
|
39
|
+
```
|
40
|
+
GET /users
|
41
|
+
```
|
42
|
+
|
43
|
+
> retrieves all users _(attributes exposed are limited by the `read_scope` method defined in the controller)_
|
44
|
+
|
45
|
+
```
|
46
|
+
GET /users/:id
|
47
|
+
```
|
48
|
+
|
49
|
+
> retrieves a user _(attributes exposed are limited by the `read_scope` method defined in the controller)_
|
50
|
+
|
51
|
+
```
|
52
|
+
POST /users
|
53
|
+
```
|
54
|
+
|
55
|
+
> creates a user _(attributes assignable are limited by the `user_params` method defined in the controller as advised here http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)_
|
56
|
+
|
57
|
+
```
|
58
|
+
PUT /users/:id
|
59
|
+
```
|
60
|
+
|
61
|
+
> updates a user _(attributes assignable are limited by the `user_params` method defined in the controller as advised here http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)_
|
62
|
+
|
63
|
+
```
|
64
|
+
DELETE /users/:id
|
65
|
+
```
|
66
|
+
|
67
|
+
> deletes a user
|
68
|
+
|
69
|
+
|
70
|
+
If your model is referenced by another model (with a `has_many`, `has_one` or `belongs_to` relationship), nested routes are also created for you. And you don't need to worry about the references/joins, they are done automaticaly!
|
71
|
+
|
72
|
+
For example, imagine a `Team` model that has many `User`s
|
73
|
+
```ruby
|
74
|
+
class Team < ActiveRecord::Base
|
75
|
+
has_many :users
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
the following routes will be exposed by the `UsersController` controller:
|
80
|
+
```
|
81
|
+
GET /team/:team_id/users
|
82
|
+
```
|
83
|
+
```
|
84
|
+
GET /team/:team_id/users/:id
|
85
|
+
```
|
86
|
+
```
|
87
|
+
POST /team/:team_id/users
|
88
|
+
```
|
89
|
+
```
|
90
|
+
PUT /team/:team_id/users/:id
|
91
|
+
```
|
92
|
+
```
|
93
|
+
DESTROY /team/:team_id/users/:id
|
94
|
+
```
|
95
|
+
|
96
|
+
### Note
|
97
|
+
You can disable __any__ of these actions by using the __::disable__ class method and providing the list of actions you want to disable
|
98
|
+
```ruby
|
99
|
+
class UsersController < Yodatra::ModelsController
|
100
|
+
disable :read, :update, :delete, :nested_read_all, :nested_delete
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Extra
|
105
|
+
You can enable a special "search" action by using the __::enable_search_on__ class method
|
106
|
+
```ruby
|
107
|
+
class UsersController < Yodatra::ModelsController
|
108
|
+
enable_search_on :name
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
## What it also provides for free
|
113
|
+
|
114
|
+
- __Logger__: Logs inside ```<your_project>/log``` in an environment named file ```env.err.log``` for all errors and ```env.log``` only for access logs.
|
115
|
+
- __Boot__: loads automaticaly all ```<your_project>/app/models/**/*.rb``` files and ```<your_project>/app/controllers/**/*.rb``` files. Establish a connection with a database by reading the ```<your_project>/config/database.yml``` file
|
116
|
+
|
117
|
+
For that create a sinatra app that inherits from ```Yodatra::Base``` instead of ```Sinatra::Base```.
|
118
|
+
|
119
|
+
## Other useful modules
|
120
|
+
|
121
|
+
- __Throttling__: To fight against the dark side, an API throttling you will need. Example: allow only 10 requests/minute per IP:
|
122
|
+
```ruby
|
123
|
+
use Yodatra::Throttle, {:redis_conf => {}, :rpm => 10}
|
124
|
+
```
|
125
|
+
_warning: this module requires redis_
|
126
|
+
- __ApiFormatter__: this middleware will help you to format all your replies. Example: wrap all you replies within a ```{data: <...>}``` object:
|
127
|
+
```ruby
|
128
|
+
use Yodatra::ApiFormatter do |status, headers, response|
|
129
|
+
body = response.empty? ? '' : response.first
|
130
|
+
response = [{:data => body}]
|
131
|
+
[status, headers, response]
|
132
|
+
end
|
133
|
+
```
|
data/Rakefile
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
|
3
|
+
|
4
|
+
require 'rdoc/task'
|
5
|
+
RDoc::Task.new do |rdoc|
|
6
|
+
rdoc.main = "README.md"
|
7
|
+
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
|
8
|
+
end
|
9
|
+
|
3
10
|
require 'rspec/core/rake_task'
|
4
11
|
RSpec::Core::RakeTask.new do |task|
|
5
12
|
task.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
@@ -4,37 +4,46 @@ module Yodatra
|
|
4
4
|
#
|
5
5
|
# Simply create your controller that inherits from this class, keeping the naming convention.
|
6
6
|
#
|
7
|
-
# For example, given a
|
8
|
-
# GET /users
|
9
|
-
# => retrieves all users <i>(attributes exposed are limited by the <b>read_scope</b> method defined in the <b>UsersController</b>)</i>
|
7
|
+
# For example, given a `User` model, creating a `class UsersController < Yodatra::ModelsController`, it will expose these routes:
|
10
8
|
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# GET /users
|
10
|
+
# > retrieves all users _(attributes exposed are limited by the <b>read_scope</b> method defined in the <b>UsersController</b>)_
|
13
11
|
#
|
14
|
-
#
|
15
|
-
#
|
12
|
+
# GET /users/:id
|
13
|
+
# > retrieves a user _(attributes exposed are limited by the `read_scope`` method defined in the `UsersController`)_
|
16
14
|
#
|
17
|
-
#
|
18
|
-
#
|
15
|
+
# POST /users
|
16
|
+
# > creates a user _(attributes assignable are limited by the `user_params` method defined in the `UsersController`)_
|
19
17
|
#
|
20
|
-
#
|
21
|
-
#
|
18
|
+
# PUT /users/:id
|
19
|
+
# > updates a user _(attributes assignable are limited by the `user_params` method defined in the `UsersController`)_
|
20
|
+
#
|
21
|
+
# DELETE /users/:id
|
22
|
+
# > deletes a user
|
22
23
|
#
|
23
24
|
# If your model is referenced by another model, nested routes are also created for you. And you don't need to worry about the references/joins, they are done automaticly!
|
24
|
-
# For example, imagine a
|
25
|
-
#
|
25
|
+
# For example, imagine a `Team` model that has many `User`s, the following routes will be exposed:
|
26
|
+
#
|
27
|
+
# GET /team/:team_id/users
|
28
|
+
#
|
29
|
+
# GET /team/:team_id/users/:id
|
30
|
+
#
|
31
|
+
# POST /team/:team_id/users
|
26
32
|
#
|
27
|
-
#
|
28
|
-
# and giving in parameters the list of actions you want to disable
|
29
|
-
# e.g. `disable :read, :read_all, :create, :update, :delete`
|
33
|
+
# PUT /team/:team_id/users/:id
|
30
34
|
#
|
31
|
-
#
|
35
|
+
# DESTROY /team/:team_id/users/:id
|
36
|
+
#
|
37
|
+
# === Note:
|
38
|
+
# You can disable any of these actions by using the __::disable__ class method
|
39
|
+
# and providing the list of actions you want to disable
|
40
|
+
# disable :read, :read_all, :create, :update, :delete, :nested_read_all, :nested_delete
|
41
|
+
#
|
42
|
+
# === Extra:
|
43
|
+
# You can enable a special "search" action by using the __::enable_search_on__ class method
|
44
|
+
# enable_search_on :name
|
32
45
|
class ModelsController < Sinatra::Base
|
33
46
|
|
34
|
-
before do
|
35
|
-
content_type 'application/json'
|
36
|
-
end
|
37
|
-
|
38
47
|
# Generic route to target ONE resource
|
39
48
|
ONE_ROUTE =
|
40
49
|
%r{\A/([\w]+?)/([0-9]+)(?:/([\w]+?)/([0-9]+)){0,1}\Z}
|
@@ -47,6 +56,10 @@ module Yodatra
|
|
47
56
|
SEARCH_ROUTE =
|
48
57
|
%r{\A/([\w]+?)(?:/([0-9]+)/([\w]+?)){0,1}/search\Z}
|
49
58
|
|
59
|
+
before do
|
60
|
+
content_type 'application/json'
|
61
|
+
end
|
62
|
+
|
50
63
|
READ_ALL = :read_all
|
51
64
|
get ALL_ROUTE do
|
52
65
|
retrieve_resources READ_ALL do |resource|
|
@@ -65,13 +78,13 @@ module Yodatra
|
|
65
78
|
post ALL_ROUTE do
|
66
79
|
retrieve_resources CREATE_ONE do |resource|
|
67
80
|
hash = self.send("#{model_name.underscore}_params".to_sym)
|
68
|
-
@one = resource.
|
81
|
+
@one = resource.create hash
|
69
82
|
|
70
|
-
if @one.
|
71
|
-
@one.as_json(read_scope).to_json
|
72
|
-
else
|
83
|
+
if @one.id.nil?
|
73
84
|
status 400
|
74
85
|
@one.errors.full_messages.to_json
|
86
|
+
else
|
87
|
+
@one.as_json(read_scope).to_json
|
75
88
|
end
|
76
89
|
end
|
77
90
|
end
|
@@ -101,28 +114,6 @@ module Yodatra
|
|
101
114
|
end
|
102
115
|
end
|
103
116
|
|
104
|
-
# Defines a nested route or not and retrieves the correct resource (or resources)
|
105
|
-
# @param disables is the name to check if it was disabled
|
106
|
-
# @param &block to be yield with the retrieved resource
|
107
|
-
def retrieve_resources(disables)
|
108
|
-
pass unless involved?
|
109
|
-
no_route if disabled? disables
|
110
|
-
|
111
|
-
model = model_name.constantize
|
112
|
-
nested = nested_resources if nested?
|
113
|
-
|
114
|
-
if model.nil? || nested.nil? && nested?
|
115
|
-
raise ActiveRecord::RecordNotFound
|
116
|
-
else
|
117
|
-
model = nested if nested?
|
118
|
-
one_id = nested? ? params[:captures].fourth : params[:captures].second if params[:captures].length == 4
|
119
|
-
model = model.find one_id unless one_id.nil?
|
120
|
-
yield(model)
|
121
|
-
end
|
122
|
-
rescue ActiveRecord::RecordNotFound
|
123
|
-
record_not_found
|
124
|
-
end
|
125
|
-
|
126
117
|
class << self
|
127
118
|
def model_name
|
128
119
|
self.name.split('::').last.gsub(/sController/, '')
|
@@ -143,6 +134,11 @@ module Yodatra
|
|
143
134
|
end
|
144
135
|
end
|
145
136
|
|
137
|
+
# This class method enables the search routes `/resoures/search?q=search+terms` for the model.
|
138
|
+
# The search will be performed on all the attributes given in parameter of this method.
|
139
|
+
# E.g. if you enabled the search on `:name` and `:email` attrs
|
140
|
+
# a GET /resources/search?q=john+doe
|
141
|
+
# will return all `Resource` instance where the name or the email matches either "john" or "doe"
|
146
142
|
def enable_search_on(*attributes)
|
147
143
|
self.instance_eval do
|
148
144
|
get SEARCH_ROUTE do
|
@@ -170,6 +166,28 @@ module Yodatra
|
|
170
166
|
|
171
167
|
private
|
172
168
|
|
169
|
+
# Defines a nested route or not and retrieves the correct resource (or resources)
|
170
|
+
# @param disables is the name to check if it was disabled
|
171
|
+
# @param &block to be yield with the retrieved resource
|
172
|
+
def retrieve_resources(disables)
|
173
|
+
pass unless involved?
|
174
|
+
no_route if disabled? disables
|
175
|
+
|
176
|
+
model = model_name.constantize
|
177
|
+
nested = nested_resources if nested?
|
178
|
+
|
179
|
+
if model.nil? || nested.nil? && nested?
|
180
|
+
raise ActiveRecord::RecordNotFound
|
181
|
+
else
|
182
|
+
model = nested if nested?
|
183
|
+
one_id = nested? ? params[:captures].fourth : params[:captures].second if params[:captures].length == 4
|
184
|
+
model = model.find one_id unless one_id.nil?
|
185
|
+
yield(model)
|
186
|
+
end
|
187
|
+
rescue ActiveRecord::RecordNotFound
|
188
|
+
record_not_found
|
189
|
+
end
|
190
|
+
|
173
191
|
def nested?
|
174
192
|
params[:captures].length >= 3 && params[:captures].first(3).none?(&:nil?)
|
175
193
|
end
|
@@ -207,7 +225,7 @@ module Yodatra
|
|
207
225
|
end
|
208
226
|
|
209
227
|
def involved?
|
210
|
-
!involved.match(
|
228
|
+
!involved.match(/\A#{model_name.underscore}[s]?\Z/).nil?
|
211
229
|
end
|
212
230
|
|
213
231
|
# read_scope defaults to all attrs of the model
|
data/lib/yodatra/version.rb
CHANGED
data/spec/data/model.rb
CHANGED
@@ -39,6 +39,19 @@ describe 'Model controller' do
|
|
39
39
|
expect(last_response).to_not be_ok
|
40
40
|
end
|
41
41
|
end
|
42
|
+
context 'when a bad route is requested' do
|
43
|
+
before do
|
44
|
+
class Yodatra::ModelsController
|
45
|
+
def read_all_disabled?; false; end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
it 'fails with no route' do
|
49
|
+
get '/amodels'
|
50
|
+
|
51
|
+
expect(last_response).to_not be_ok
|
52
|
+
expect(last_response.body).to eq('<h1>Not Found</h1>')
|
53
|
+
end
|
54
|
+
end
|
42
55
|
end
|
43
56
|
describe 'getting an specific Model instance' do
|
44
57
|
it 'should have a GET one route' do
|
@@ -70,6 +83,15 @@ describe 'Model controller' do
|
|
70
83
|
expect(last_response).to be_ok
|
71
84
|
end
|
72
85
|
end
|
86
|
+
context 'on a nested object' do
|
87
|
+
it 'creates an instance, saves it and succeed' do
|
88
|
+
expect{
|
89
|
+
post '/models/1/models', {:data=> 'e'}
|
90
|
+
}.to change(Model::ALL, :length).by(1)
|
91
|
+
|
92
|
+
expect(last_response).to be_ok
|
93
|
+
end
|
94
|
+
end
|
73
95
|
context 'with incorrect params' do
|
74
96
|
it 'does not create an instance and fails' do
|
75
97
|
expect{
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yodatra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Bonaud
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|