yodatra 0.3.5 → 0.3.6
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 +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
|