rack-autocrud 0.1.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.
Files changed (4) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +119 -0
  3. data/lib/rack/autocrud.rb +152 -0
  4. metadata +99 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012, Tim Hentenaar
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ rack-autocrud
2
+ =============
3
+
4
+ Rack middleware that works with Sinatra and DataMapper to dynamically
5
+ create CRUD endpoints and routes based on models. It ain't perfect, but
6
+ it works.
7
+
8
+ These generated CRUD routes are assumed to return a Rack response.
9
+
10
+ It's important to note, that you models and endpoints must be in separate
11
+ modules (read: namespaces).
12
+
13
+ Input and Response data are formatted as JSON.
14
+
15
+ Licensing
16
+ =========
17
+
18
+ This software is licensed under the [Simplified BSD License](http://en.wikipedia.org/wiki/BSD_licenses#2-clause_license_.28.22Simplified_BSD_License.22_or_.22FreeBSD_License.22.29) as described in the LICENSE file.
19
+
20
+ Requirements
21
+ ============
22
+
23
+ * sinatra
24
+ * datamapper
25
+
26
+ Installation
27
+ ============
28
+
29
+ gem install rack-autocrud
30
+
31
+ Usage
32
+ =====
33
+
34
+ Just add something like this to your _config.ru_:
35
+
36
+ ```ruby
37
+ require 'rack/autocrud'
38
+
39
+ # Load your models
40
+ require 'models'
41
+
42
+ # Load your endpoints
43
+ require 'endpoints'
44
+
45
+ # Auto-magical CRUD
46
+ run Rack::AutoCRUD.new nil, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints'
47
+ ```
48
+
49
+ This would assume you only want CRUD-based routing. You can also _use_ this middleware:
50
+
51
+ ```ruby
52
+ use Rack::AutoCRUD, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints'
53
+ ```
54
+
55
+ How Routing Works
56
+ =================
57
+
58
+ The routing is simple. You have a model *Models::Person*. You've added something like the above to your
59
+ _config.ru_. This middleware will dynamically create a _Sinatra::Base_ subclass called *Endpoints::Person*
60
+ (if it already exists, these routes are added to it) which will contain the following routes:
61
+
62
+ | Route | Action | HTTP Response Code |
63
+ | ----------- | -------------------------------| ------------------ |
64
+ | get / | List all _Person_ entries | 403 |
65
+ | post / | Create a new _Person_ | 201 / 402 |
66
+ | get /:id | Retrieve a _Person_ | 200 |
67
+ | put /:id | Update a _Person_ | 201 / 403 |
68
+ | delete /:id | Destroy a _Person_ | 204 |
69
+
70
+ The middleware will route based on the URI. Thus, _/person_ would correspond to *Endpoints::Person*'s _get /_ route.
71
+
72
+ Overriding Generated Routes
73
+ ===========================
74
+
75
+ You can define your own CRUD routes, which will be called and return a response
76
+ before the autogenerated routes, as long as they're added after your endpoint is defined.
77
+
78
+ For example:
79
+
80
+ ```ruby
81
+ require 'sinatra/base'
82
+
83
+ module Endpoints
84
+ class Person < Sinatra::Base
85
+ get '/'
86
+ Models::Person.all.to_json
87
+ end
88
+ end
89
+ ```
90
+
91
+ In this case, if you're using _dm-serializer_,you'd get back every _Models::Person_ record in the database in
92
+ a JSON array. By default, the _get /_ route returns "Access Denied."
93
+
94
+ CRUD Processing Hooks
95
+ =====================
96
+
97
+ There are some basic processing hooks you can define in your endpoint:
98
+
99
+ | Hook | Description |
100
+ | ------------------------------ | ---------------------------------------------------------------- |
101
+ | pre_create(env,request,obj) | Called after the record is created, but before it's saved |
102
+ | post_create(env,request,obj) | Called after the record is saved, if it was saved successfully |
103
+ | pre_retrieve(env,request) | Called before the record is fetched |
104
+ | post_retrieve(env,request,obj) | Called after the record is fetched |
105
+ | pre_update(env,request) | Called before the record is updated |
106
+ | post_update(env,request) | Called after the record is updated, if it was saved successfully |
107
+ | pre_destroy(env,request) | Called before the record is destroyed |
108
+ | post_destroy(env,request,obj) | Called after the record is destroyed |
109
+
110
+ Parameters:
111
+
112
+ * *env* is the current Rack environment
113
+ * *request* is the current request object
114
+ * *obj* is the ORM object corresponding to the record in question
115
+
116
+ If any of these hooks returns anything other than _nil_, it is assumed to be a response object, which
117
+ is returned immediately, and no further processing is performed.
118
+
119
+
@@ -0,0 +1,152 @@
1
+ #
2
+ # Rack::AutoCRUD - AutoCRUD Middleware for Rack
3
+ #
4
+ # Copyright (C) 2012 Tim Hentenaar. All Rights Reserved.
5
+ #
6
+ # Licensed under the Simplified BSD License.
7
+ # See the LICENSE file for details.
8
+ #
9
+ # This Rack middleware automatically generates Sinatra
10
+ # endpoints (descended from Sinatra::Base) to handle
11
+ # basic CRUD operations on defined models.
12
+ #
13
+
14
+ require 'sinatra/base'
15
+ require 'json'
16
+
17
+ module Rack
18
+ class AutoCRUD
19
+ def initialize(app,options={})
20
+ @app = app
21
+ @model_namespace = options[:model_namespace]
22
+ @endpoint_namespace = options[:endpoint_namespace]
23
+ @endpoint_mod = nil
24
+ end
25
+
26
+ def call(env)
27
+ dup._call(env) # For thread safety...
28
+ end
29
+
30
+ def _call(env)
31
+ model_klass = nil
32
+ endpoint_klass = nil
33
+ verb,endpoint,*uri = env['REQUEST_URI'].split('/')
34
+ verb = env['REQUEST_METHOD'].downcase
35
+
36
+ # Enumerate through all defined classes, checking for the model / endpoint
37
+ ObjectSpace.each_object(Class) { |klass|
38
+ model_klass = klass if String(klass.name).downcase == String(@model_namespace + '::' + endpoint).downcase
39
+ endpoint_klass = klass if String(klass.name).downcase == String(@endpoint_namespace + '::' + endpoint).downcase
40
+ }
41
+
42
+ # Lazily locate the endpoint namespace module (if we haven't already)
43
+ if endpoint_klass.nil? && @endpoint_mod.nil?
44
+ ObjectSpace.each_object(Module) { |klass|
45
+ @endpoint_mod = klass if String(klass.name).downcase == @endpoint_namespace.downcase
46
+ }
47
+ end
48
+
49
+ # Now, if we've got something, do our magic.
50
+ if !model_klass.nil?
51
+ # If we don't have an endpoint class, make one
52
+ if endpoint_klass.nil?
53
+ endpoint_klass = Class.new(Sinatra::Base)
54
+ @endpoint_mod.const_set(endpoint.capitalize,endpoint_klass)
55
+ end
56
+
57
+ # Patch in the routes
58
+ endpoint_klass.class_exec(model_klass,endpoint,env) { |model,endpoint,env|
59
+ get '/' do
60
+ halt [403, '{ "error": "Access Denied" }']
61
+ end
62
+
63
+ post '/' do
64
+ obj = model.new(JSON.parse(request.body.read))
65
+
66
+ # Call the pre-create hook
67
+ if self.respond_to?(:pre_create)
68
+ ret = pre_create(env,request,obj)
69
+ return ret unless ret.nil?
70
+ end
71
+
72
+ obj.save
73
+ halt [402, '{ "error": "Failed to save ' + endpoint + '" }'] unless obj.saved?
74
+
75
+ # Call the post-create hook
76
+ if self.respond_to?(:post_create)
77
+ ret = post_create(env,request,obj)
78
+ return ret unless ret.nil?
79
+ end
80
+
81
+ [ 201, { 'id' => obj.id.to_i }.to_json ]
82
+ end
83
+
84
+ get '/:id' do
85
+ # Call the pre-retrieve hook
86
+ if self.respond_to?(:pre_retrieve)
87
+ ret = pre_retrieve(env,request)
88
+ return ret unless ret.nil?
89
+ end
90
+
91
+ obj = model.get(params[:id])
92
+
93
+ # Call the post-retrieve hook
94
+ if self.respond_to?(:post_retrieve)
95
+ ret = post_retrieve(env,request,obj)
96
+ return ret unless ret.nil?
97
+ end
98
+
99
+ obj.to_json
100
+ end
101
+
102
+ put '/:id' do
103
+ # Call the pre-update hook
104
+ if self.respond_to?(:pre_update)
105
+ ret = pre_update(env,request)
106
+ return ret unless ret.nil?
107
+ end
108
+
109
+ saved = model.update(JSON.parse(request.body.read))
110
+ halt [403, 'Access Denied'] unless saved
111
+
112
+ # Call the post-update hook
113
+ if self.respond_to?(:post_update)
114
+ ret = post_update(env,request)
115
+ return ret unless ret.nil?
116
+ end
117
+
118
+ [ 201, '{ "status": "ok" }' ]
119
+ end
120
+
121
+ delete '/:id' do
122
+ # Call the pre-destroy hook
123
+ if self.respond_to?(:pre_destroy)
124
+ ret = pre_destroy(env,request)
125
+ return ret unless ret.nil?
126
+ end
127
+
128
+ obj = model.get(params[:id])
129
+ obj.destroy if obj
130
+
131
+ # Call the post-destroy hook
132
+ if self.respond_to?(:post_destroy)
133
+ ret = post_destroy(env,request,obj)
134
+ return ret unless ret.nil?
135
+ end
136
+
137
+ [ 204 ]
138
+ end
139
+ }
140
+
141
+ # Now, call the endpoint class (assuming it will return a response)
142
+ env['PATH_INFO'] = '/' + uri.join('/')
143
+ return endpoint_klass.call(env)
144
+ end
145
+
146
+ # Otherwise, pass the request down the chain...
147
+ @app.call(env)
148
+ end
149
+ end
150
+ end
151
+
152
+ # vi:set ts=2:
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-autocrud
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Hentenaar
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: data_mapper
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: ! "\tRack middleware that works with Sinatra and DataMapper to dynamically\n\tcreate
63
+ CRUD endpoints and routes based on models. It ain't perfect, but\n\tit works.\n\n\tThese
64
+ generated CRUD routes are assumed to return a Rack response.\n\n\tIt's important
65
+ to note, that you models and endpoints must be in separate\n\tmodules (read: namespaces).\n\n\tInput
66
+ and Response data are formatted as JSON.\n\n\tSee the README for more info.\n"
67
+ email: tim.hentenaar@gmail.com
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - lib/rack/autocrud.rb
73
+ - README.md
74
+ - LICENSE
75
+ homepage: https://github.com/thentenaar/rack-autocrud
76
+ licenses: []
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.24
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Rack middleware that automagically handles basic CRUD operations
99
+ test_files: []