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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c28dec8ba275bce4a44a79d41f2c3aac9a265e5e
4
- data.tar.gz: f516d481e2e6ad9ce2c49ab3805584f1e0bfa270
3
+ metadata.gz: e6ce8fae3026a73118bcc2345e60dc4e1e93eabc
4
+ data.tar.gz: d6f0c9ac4382981d4fd48d8baec3864ed3018f0d
5
5
  SHA512:
6
- metadata.gz: bb2fb3841624fb4f869286779ed5c7a1b28e84165b8dca7f09cb3a577c7f01acb1f0fc851ceb143bef3d3ede66eff5b8a6a55d72aef1a07f8d4964b00e9f5a31
7
- data.tar.gz: a97295b4414f0939ec2d9ad263ecfadd6b807a80130cfc5642f781a955fadc88e32867e4ef5da82418719b633f59aaefb4d5fae5124401bf37450e528f98ed71
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
- Visiting [https://squareteam.github.io/yodatra](https://squareteam.github.io/yodatra) will help you to control the force.
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 <b>User</b> model, creating a <b>class UsersController < Yodatra::ModelsController</b>, it will expose these routes:
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
- # GET /users/:id
12
- # => retrieves a user <i>(attributes exposed are limited by the <b>read_scope</b> method defined in the <b>UsersController</b>)</i>
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
- # POST /users
15
- # => creates a user <i>(attributes assignable are limited by the <b>user_params</b> method defined in the <b>UsersController</b>)</i>
12
+ # GET /users/:id
13
+ # > retrieves a user _(attributes exposed are limited by the `read_scope`` method defined in the `UsersController`)_
16
14
  #
17
- # PUT /users/:id
18
- # => updates a user <i>(attributes assignable are limited by the <b>user_params</b> method defined in the <b>UsersController</b>)</i>
15
+ # POST /users
16
+ # > creates a user _(attributes assignable are limited by the `user_params` method defined in the `UsersController`)_
19
17
  #
20
- # DELETE /users/:id
21
- # => deletes a user
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 <b>Team</b> model that has many <b>User</b>s, the following routes will be exposed:
25
- # GET /team/:team_id/users, GET /team/:team_id/users/:id, POST /team/:team_id/users, PUT /team/:team_id/users/:id and DESTROY /team/:team_id/users/:id
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
- # _Note_: You can disable any of these five actions by using the `#disable` class method
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
- # _Note2_: You can enable a special "search" action by using the `#enable_search_on` class method
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.new hash
81
+ @one = resource.create hash
69
82
 
70
- if @one.save
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(/#{model_name.underscore}[s]?/).nil?
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
@@ -1,3 +1,3 @@
1
1
  module Yodatra
2
- VERSION = '0.3.5'
2
+ VERSION = '0.3.6'
3
3
  end
@@ -35,9 +35,9 @@ module ActiveRecord
35
35
  options[:null])
36
36
  end
37
37
 
38
- def columns(table_name, message)
38
+ def columns(table_name, message=nil)
39
39
  @columns[table_name]
40
40
  end
41
41
  end
42
42
  end
43
- end
43
+ end
data/spec/data/model.rb CHANGED
@@ -12,6 +12,7 @@ class Model
12
12
  end
13
13
  def create(param); me = self.new(param); me.save; me; end
14
14
  end
15
+ def id; ALL.index @data; end
15
16
  def initialize(param); @data = param[:data]; self; end
16
17
  def save
17
18
  unless @data.nil? || @data.match(/[\d]+/)
@@ -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.5
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-08-23 00:00:00.000000000 Z
11
+ date: 2014-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack