opi 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1d5fda4ffbb7acce5d6c4a7f0f0fcc91d9c1cd2f
4
+ data.tar.gz: 1e27c75791cd7020ae3e94657dca5b42eedeebd6
5
+ SHA512:
6
+ metadata.gz: 3628bf167bae180b00fc035355e8841b0840eeed852c4e7d040563198fb3b24ac531c637066d40937b1b39692b6ea96954c8a635378e03dc5c39b968a240984c
7
+ data.tar.gz: 12ebe8eb10d4f4144f841051d03d8a11d6331363d47d8332f4d54e98b61c64ebe44f3bb3bf0b2a734ad210ea52b14a4ee8de8c6a948b97e9d7f20d98d4a68f94
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Richard Taylor
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,99 @@
1
+ h1. Opi - The very opinionated API service library
2
+
3
+ h2. Install the gem
4
+
5
+ gem 'opi'
6
+
7
+ h2. About
8
+
9
+ Opi is a very opinionated rack-compliant API service library. In fact, it is
10
+ so opinionated it is very likely to offend. But that is alright.
11
+
12
+ Opi was born out of frustration with writing too much boilerplate code for api
13
+ services.
14
+
15
+ *JSON-only*. The server CANNOT respond with anything else other than JSON.
16
+ All error responses are JSON out of the box. You CANNOT respond with HTML.
17
+ The response content type is hardcoded to JSON.
18
+
19
+ *No Controllers*. Well.. there are route blocks which are the equivalent
20
+ of the controller but you are strongly encouraged to only ever execute actions
21
+ in these blocks (the server is looking out for those actions as responses).
22
+ The only role of the 'controller' here is to map HTTP inputs to Action inputs.
23
+
24
+ *Action-based*. All logic is an action. Actions validate their own inputs
25
+ and have no access to anything HTTP-related. These are domain-specific actions
26
+ that can be pulled out and used anywhere.
27
+
28
+ *No Sessions or Cookies*. None.
29
+
30
+ But this has its advantages. It is *fast* and *simple*.
31
+
32
+ h2. Example
33
+
34
+ This simple example doesn't really go into the detail of Actions but it does
35
+ demonstrate the routes, responses, before filters and helpers.
36
+
37
+ <code>api.rb</code>
38
+
39
+ <pre><code>module Example
40
+ class API < Opi::API
41
+
42
+ before :authorize!
43
+
44
+ get '/ping', :skip => :authorize! do
45
+ {:pong => Time.now.to_i}
46
+ end
47
+
48
+ get '/meaning' do
49
+ {:answer => 42}
50
+ end
51
+
52
+ helpers do
53
+ def authorize!
54
+ error!('401 Unauthorized', 401) unless params['secret'] == '1234'
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ </code></pre>
61
+
62
+ <code>config.ru</code>
63
+
64
+ <pre><code>require 'opi'
65
+
66
+ load './api.rb'
67
+ run Example::API.new
68
+ </code></pre>
69
+
70
+ <code>output</code>
71
+
72
+ <pre><code>$ curl -i http://0.0.0.0:9292/ping
73
+ HTTP/1.1 200 OK
74
+ Content-Type: application/json
75
+ Transfer-Encoding: chunked
76
+
77
+ {"pong":1387810814}
78
+
79
+ $ curl -i http://0.0.0.0:9292/meaning
80
+ HTTP/1.1 401 Unauthorized
81
+ Content-Type: application/json
82
+ Transfer-Encoding: chunked
83
+
84
+ {"error":"401 Unauthorized"}
85
+
86
+ $ curl -i http://0.0.0.0:9292/meaning?secret=1234
87
+ HTTP/1.1 200 OK
88
+ Content-Type: application/json
89
+ Transfer-Encoding: chunked
90
+
91
+ {"answer":42}
92
+
93
+ $ curl -i http://0.0.0.0:9292/nonexistant
94
+ HTTP/1.1 404 Not Found
95
+ Content-Type: application/json
96
+ Transfer-Encoding: chunked
97
+
98
+ {"error":"404 Not Found"}
99
+ </code></pre>
data/lib/opi.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'rack'
2
+ require 'colored'
3
+ require 'json'
4
+ require 'mutations'
5
+
6
+ require_relative './opi/version'
7
+ require_relative './opi/api'
8
+ require_relative './opi/request'
9
+ require_relative './opi/response'
10
+ require_relative './opi/action'
11
+ require_relative './opi/context'
12
+ require_relative './opi/loader'
13
+
14
+ module Opi
15
+ VERSION = '1.0'
16
+ end
data/lib/opi/action.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Opi
2
+ class Action < Mutations::Command
3
+ end
4
+ end
data/lib/opi/api.rb ADDED
@@ -0,0 +1,75 @@
1
+ module Opi
2
+ class API
3
+
4
+ class << self
5
+ def get(path, options={}, &block)
6
+ route 'GET', path, options, block
7
+ end
8
+
9
+ def post(path, options={}, &block)
10
+ route 'POST', path, options, block
11
+ end
12
+
13
+ def put(path, &block)
14
+ route 'PUT', path, options, block
15
+ end
16
+
17
+ def delete(path, options={}, &block)
18
+ route 'DELETE', path, options, block
19
+ end
20
+
21
+ def route(method, path, options={}, block)
22
+ # TODO: remove&replace existing routes (on reload)
23
+ routes.unshift({:method => method, :path => path, :options => options, :block => block})
24
+ end
25
+
26
+ def before(method)
27
+ before_filters << method
28
+ end
29
+
30
+ def after(method)
31
+ after_filters << method
32
+ end
33
+
34
+ def before_filters
35
+ @before_filters ||= []
36
+ end
37
+
38
+ def after_filters
39
+ @after_filters ||= []
40
+ end
41
+
42
+ def routes
43
+ @routes ||= []
44
+ end
45
+
46
+ def helpers(&block)
47
+ mod = Module.new
48
+ mod.class_eval &block
49
+ Context.send :include, mod
50
+ end
51
+
52
+ end
53
+
54
+ def call(env)
55
+ begin
56
+ Loader.reload!
57
+
58
+ request = Request.new(env)
59
+
60
+ route = self.class.routes.detect{|x| x[:method] == request.method and x[:path] == request.path}
61
+
62
+ return [404, {'Content-Type' => 'application/json'}, ["{\"error\":\"404 Not Found\"}", "\n"]] unless route
63
+
64
+ context = Context.new(env, route, request, self.class.before_filters, self.class.after_filters)
65
+ response = context.run
66
+
67
+ [response.status, response.header, response.body]
68
+ rescue Exception => e
69
+ return [500, {'Content-Type' => 'application/json'}, ["{\"error\":\"500 Internal Server Error\", \"message\":\"#{e.message}\"}", "\n"]]
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,63 @@
1
+ module Opi
2
+ class Context
3
+
4
+ attr_reader :env, :route, :request, :response, :before, :after, :error
5
+
6
+ def initialize(env, route, request, before, after)
7
+ @env = env
8
+ @route = route
9
+ @request = request
10
+ @response = Response.new
11
+ @before = before
12
+ @after = after
13
+ @error = nil
14
+ end
15
+
16
+ def params
17
+ @request.params
18
+ end
19
+
20
+ def error!(message, status)
21
+ @error = {:message => message, :status => status}
22
+ end
23
+
24
+ def run
25
+ skip = route[:options][:skip] || []
26
+ skip = [skip] unless skip.is_a? Array
27
+
28
+ route_before = route[:options][:before] || []
29
+ route_before = [route_before] unless route_before.is_a? Array
30
+
31
+ (self.before + route_before).each do |before|
32
+ next if skip.include? before
33
+
34
+ self.send before # execute before filter
35
+
36
+ if self.error
37
+ response.body = ["{\"error\":\"#{error[:message]}\"}", "\n"]
38
+ response.status = error[:status]
39
+ return response
40
+ end
41
+ end
42
+
43
+ # before filters must have succeeded
44
+ action = instance_eval &route[:block]
45
+
46
+ if action.is_a? Opi::Action
47
+ if action.success?
48
+ response.status = 200
49
+ response.body = [action.result.to_json, "\n"]
50
+ else
51
+ response.status = 400
52
+ response.body = [action.errors.symbolic.to_json, "\n"]
53
+ end
54
+ else
55
+ response.status = 200
56
+ response.body = [action.to_json, "\n"]
57
+ end
58
+
59
+ response
60
+ end
61
+
62
+ end
63
+ end
data/lib/opi/loader.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Opi
2
+ class Loader
3
+
4
+ class << self
5
+
6
+ def loadcache()
7
+ @loadcache ||= {}
8
+ end
9
+
10
+ def funkyload(file)
11
+ begin
12
+ if cache = loadcache[file]
13
+ return if ENV['RACK_ENV'] == 'production'
14
+
15
+ if (mtime = File.mtime(file)) > cache
16
+ puts "[Opi::Loader]".green + " reloading: #{file}"
17
+ load file
18
+ loadcache[file] = mtime
19
+ end
20
+ else
21
+ puts "[Opi::Loader]".green + " loading: #{file}"
22
+ load file
23
+ loadcache[file] = File.mtime(file)
24
+ end
25
+ rescue Exception => e
26
+ puts "[Opi::Loader] Exception loading class [#{file}]: #{e.message}"
27
+ puts e.backtrace.join("\n")
28
+ raise e
29
+ end
30
+ end
31
+
32
+ def reload!
33
+ Dir["#{@prefix}lib/*.rb"].each { |x| funkyload x }
34
+ Dir["#{@prefix}actions/**/*.rb"].each { |x| funkyload x }
35
+ Dir["#{@prefix}*.rb"].each { |x| funkyload x unless x == 'Rakefile.rb' }
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module Opi
2
+ class Request < Rack::Request
3
+
4
+ def user_agent
5
+ @env['HTTP_USER_AGENT']
6
+ end
7
+
8
+ def accept
9
+ @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip }
10
+ end
11
+
12
+ def path
13
+ @env["REQUEST_PATH"]
14
+ end
15
+
16
+ def uri
17
+ @env["REQUEST_URI"]
18
+ end
19
+
20
+ def method
21
+ @env['REQUEST_METHOD']
22
+ end
23
+
24
+ def path_components
25
+ @path_components ||= path.split('/')
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module Opi
2
+ class Response < Rack::Response
3
+
4
+ def initialize
5
+ @status, @body = 200, ["{}","\n"]
6
+ @header = Rack::Utils::HeaderHash.new({
7
+ 'Content-Type' => 'application/json'
8
+ # 'Server' => 'TBD/1.0'
9
+ })
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Opi
2
+ VERSION = "0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opi
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Richard Taylor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.5.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.5.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: colored
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.8.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: mutations
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.6.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: 0.6.0
69
+ description: The very opinionated API service library.
70
+ email: moomerman@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - README.textile
76
+ - LICENSE
77
+ - lib/opi.rb
78
+ - lib/opi/action.rb
79
+ - lib/opi/api.rb
80
+ - lib/opi/context.rb
81
+ - lib/opi/loader.rb
82
+ - lib/opi/request.rb
83
+ - lib/opi/response.rb
84
+ - lib/opi/version.rb
85
+ homepage: http://github.com/moomerman/opi
86
+ licenses: []
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options:
90
+ - --inline-source
91
+ - --charset=UTF-8
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project: opi
106
+ rubygems_version: 2.0.0
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: The very opinionated API service library.
110
+ test_files: []