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 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