opi 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{README.textile → README.md} +41 -16
- data/lib/opi.rb +1 -1
- data/lib/opi/api.rb +9 -4
- data/lib/opi/context.rb +2 -2
- data/lib/opi/router.rb +32 -0
- data/lib/opi/version.rb +2 -2
- metadata +17 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c83ed04de99dfd5352546b0548455cd791b4434c
|
4
|
+
data.tar.gz: 436349cc1d191ae3d38447f991fdac2f64ef26c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d446295c4c31a858d8d0a1795c4fc87e864fb47bad18b143349f3a20b7138165fda6669ed1d04f3ea512ba730464eeb81152bc8722dda339b2e006c812a59d2
|
7
|
+
data.tar.gz: 25f21eae30b44928e3af096159e39bda11f7d0ee2d098d1873e4337a8b6ef56f2df2dc191876ac84fcf008569fad7f55b84bec0c10be42b0cac43afc7875fd88
|
@@ -1,10 +1,10 @@
|
|
1
|
-
|
1
|
+
# Opi - The very opinionated API service library
|
2
2
|
|
3
|
-
|
3
|
+
## Install the gem
|
4
4
|
|
5
5
|
gem 'opi'
|
6
6
|
|
7
|
-
|
7
|
+
## About
|
8
8
|
|
9
9
|
Opi is a very opinionated rack-compliant API service library. In fact, it is
|
10
10
|
so opinionated it is very likely to offend. But that is alright.
|
@@ -12,64 +12,75 @@ so opinionated it is very likely to offend. But that is alright.
|
|
12
12
|
Opi was born out of frustration with writing too much boilerplate code for api
|
13
13
|
services.
|
14
14
|
|
15
|
-
|
15
|
+
**JSON-only**. The server CANNOT respond with anything else other than JSON.
|
16
16
|
All error responses are JSON out of the box. You CANNOT respond with HTML.
|
17
17
|
The response content type is hardcoded to JSON.
|
18
18
|
|
19
|
-
|
19
|
+
**No Controllers**. Well.. there are route blocks which are the equivalent
|
20
20
|
of the controller but you are strongly encouraged to only ever execute actions
|
21
21
|
in these blocks (the server is looking out for those actions as responses).
|
22
22
|
The only role of the 'controller' here is to map HTTP inputs to Action inputs.
|
23
23
|
|
24
|
-
|
24
|
+
**Action-based**. All logic is an action. Actions validate their own inputs
|
25
25
|
and have no access to anything HTTP-related. These are domain-specific actions
|
26
26
|
that can be pulled out and used anywhere.
|
27
27
|
|
28
|
-
|
28
|
+
**No Sessions or Cookies**. None.
|
29
29
|
|
30
30
|
But this has its advantages. It is *fast* and *simple*.
|
31
31
|
|
32
|
-
|
32
|
+
## Example
|
33
33
|
|
34
34
|
This simple example doesn't really go into the detail of Actions but it does
|
35
35
|
demonstrate the routes, responses, before filters and helpers.
|
36
36
|
|
37
37
|
<code>api.rb</code>
|
38
38
|
|
39
|
-
|
39
|
+
```ruby
|
40
|
+
module Example
|
40
41
|
class API < Opi::API
|
41
42
|
|
42
43
|
before :authorize!
|
43
44
|
|
45
|
+
get '/boom', :skip => :authorize! do
|
46
|
+
raise 'boom'
|
47
|
+
end
|
48
|
+
|
44
49
|
get '/ping', :skip => :authorize! do
|
45
50
|
{:pong => Time.now.to_i}
|
46
51
|
end
|
47
52
|
|
48
|
-
get '/
|
53
|
+
get '/users/:id' do
|
54
|
+
params
|
55
|
+
end
|
56
|
+
|
57
|
+
post '/meaning' do
|
49
58
|
{:answer => 42}
|
50
59
|
end
|
51
60
|
|
52
61
|
helpers do
|
53
62
|
def authorize!
|
54
|
-
error!('401 Unauthorized', 401) unless params[
|
63
|
+
error!('401 Unauthorized', 401) unless params[:secret] == '1234'
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
58
67
|
end
|
59
68
|
end
|
60
|
-
|
69
|
+
```
|
61
70
|
|
62
71
|
<code>config.ru</code>
|
63
72
|
|
64
|
-
|
73
|
+
```ruby
|
74
|
+
require 'opi'
|
65
75
|
|
66
76
|
load './api.rb'
|
67
77
|
run Example::API.new
|
68
|
-
|
78
|
+
```
|
69
79
|
|
70
80
|
<code>output</code>
|
71
81
|
|
72
|
-
|
82
|
+
```bash
|
83
|
+
$ curl -i http://0.0.0.0:9292/ping
|
73
84
|
HTTP/1.1 200 OK
|
74
85
|
Content-Type: application/json
|
75
86
|
Transfer-Encoding: chunked
|
@@ -90,10 +101,24 @@ Transfer-Encoding: chunked
|
|
90
101
|
|
91
102
|
{"answer":42}
|
92
103
|
|
104
|
+
$ curl -i http://0.0.0.0:9292/users/1?secret=1234
|
105
|
+
HTTP/1.1 200 OK
|
106
|
+
Content-Type: application/json
|
107
|
+
Transfer-Encoding: chunked
|
108
|
+
|
109
|
+
{"secret":"1234","id":"1"}
|
110
|
+
|
93
111
|
$ curl -i http://0.0.0.0:9292/nonexistant
|
94
112
|
HTTP/1.1 404 Not Found
|
95
113
|
Content-Type: application/json
|
96
114
|
Transfer-Encoding: chunked
|
97
115
|
|
98
116
|
{"error":"404 Not Found"}
|
99
|
-
|
117
|
+
|
118
|
+
$ curl -i http://0.0.0.0:9292/boom
|
119
|
+
HTTP/1.1 500 Internal Server Error
|
120
|
+
Content-Type: application/json
|
121
|
+
Transfer-Encoding: chunked
|
122
|
+
|
123
|
+
{"error":"500 Internal Server Error", "message":"boom"}
|
124
|
+
```
|
data/lib/opi.rb
CHANGED
@@ -4,6 +4,7 @@ require 'json'
|
|
4
4
|
require 'mutations'
|
5
5
|
|
6
6
|
require_relative './opi/version'
|
7
|
+
require_relative './opi/router'
|
7
8
|
require_relative './opi/api'
|
8
9
|
require_relative './opi/request'
|
9
10
|
require_relative './opi/response'
|
@@ -12,5 +13,4 @@ require_relative './opi/context'
|
|
12
13
|
require_relative './opi/loader'
|
13
14
|
|
14
15
|
module Opi
|
15
|
-
VERSION = '1.0'
|
16
16
|
end
|
data/lib/opi/api.rb
CHANGED
@@ -2,6 +2,9 @@ module Opi
|
|
2
2
|
class API
|
3
3
|
|
4
4
|
class << self
|
5
|
+
|
6
|
+
puts "* Opi Version: #{Opi::VERSION}".green
|
7
|
+
|
5
8
|
def get(path, options={}, &block)
|
6
9
|
route 'GET', path, options, block
|
7
10
|
end
|
@@ -20,7 +23,7 @@ module Opi
|
|
20
23
|
|
21
24
|
def route(method, path, options={}, block)
|
22
25
|
# TODO: remove&replace existing routes (on reload)
|
23
|
-
routes.unshift({:method => method, :path => path, :options => options, :block => block})
|
26
|
+
router.routes.unshift({:method => method, :path => path, :options => options, :block => block})
|
24
27
|
end
|
25
28
|
|
26
29
|
def before(method)
|
@@ -39,8 +42,8 @@ module Opi
|
|
39
42
|
@after_filters ||= []
|
40
43
|
end
|
41
44
|
|
42
|
-
def
|
43
|
-
@
|
45
|
+
def router
|
46
|
+
@router ||= Router.new
|
44
47
|
end
|
45
48
|
|
46
49
|
def helpers(&block)
|
@@ -57,7 +60,9 @@ module Opi
|
|
57
60
|
|
58
61
|
request = Request.new(env)
|
59
62
|
|
60
|
-
route = self.class.
|
63
|
+
route, params = self.class.router.route(request.method, request.path)
|
64
|
+
request.params.merge!(params) if params and params.is_a? Hash
|
65
|
+
request.params.merge!('splat' => params.join(',')) if params and params.is_a? Array
|
61
66
|
|
62
67
|
return [404, {'Content-Type' => 'application/json'}, ["{\"error\":\"404 Not Found\"}", "\n"]] unless route
|
63
68
|
|
data/lib/opi/context.rb
CHANGED
@@ -14,7 +14,7 @@ module Opi
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def params
|
17
|
-
@request.params
|
17
|
+
{}.tap {|h| @request.params.each{|x| h[x.first.to_sym] = x.last}}
|
18
18
|
end
|
19
19
|
|
20
20
|
def error!(message, status)
|
@@ -43,7 +43,7 @@ module Opi
|
|
43
43
|
# before filters must have succeeded
|
44
44
|
action = instance_eval &route[:block]
|
45
45
|
|
46
|
-
if action.
|
46
|
+
if action.kind_of? Opi::Action or action.kind_of? Mutations::Outcome
|
47
47
|
if action.success?
|
48
48
|
response.status = 200
|
49
49
|
response.body = [action.result.to_json, "\n"]
|
data/lib/opi/router.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
class Router
|
2
|
+
attr_accessor :routes
|
3
|
+
|
4
|
+
WILDCARD_PATTERN = /\/\*/
|
5
|
+
NAMED_SEGMENTS_PATTERN = /\/:([^$\/]+)/
|
6
|
+
NAMED_SEGMENTS_REPLACEMENT_PATTERN = /\/:([^$\/]+)/
|
7
|
+
|
8
|
+
def initialize(routes=[])
|
9
|
+
@routes = routes
|
10
|
+
end
|
11
|
+
|
12
|
+
def route(method, path)
|
13
|
+
method_routes = self.routes.find_all{|x| x[:method] == method}
|
14
|
+
method_routes.each do |route|
|
15
|
+
if route[:path] =~ WILDCARD_PATTERN
|
16
|
+
src = "\\A#{route[:path].gsub('*','(.*)')}\\Z"
|
17
|
+
if match = path.match(Regexp.new(src))
|
18
|
+
return [route, match[1].split('/')]
|
19
|
+
end
|
20
|
+
elsif route[:path] =~ NAMED_SEGMENTS_PATTERN
|
21
|
+
src = "\\A#{route[:path].gsub(NAMED_SEGMENTS_REPLACEMENT_PATTERN, '/(?<\1>[^$/]+)')}\\Z"
|
22
|
+
if match = path.match(Regexp.new(src))
|
23
|
+
return [route, Hash[match.names.zip(match.captures)]]
|
24
|
+
end
|
25
|
+
elsif path == route[:path]
|
26
|
+
return [route]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/opi/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Opi
|
2
|
-
VERSION = "0.
|
3
|
-
end
|
2
|
+
VERSION = "0.2"
|
3
|
+
end
|
metadata
CHANGED
@@ -1,69 +1,69 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Taylor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 1.5.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.5.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: colored
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: json
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 1.8.1
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 1.8.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: mutations
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 0.6.0
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.6.0
|
69
69
|
description: The very opinionated API service library.
|
@@ -72,8 +72,8 @@ executables: []
|
|
72
72
|
extensions: []
|
73
73
|
extra_rdoc_files: []
|
74
74
|
files:
|
75
|
-
- README.textile
|
76
75
|
- LICENSE
|
76
|
+
- README.md
|
77
77
|
- lib/opi.rb
|
78
78
|
- lib/opi/action.rb
|
79
79
|
- lib/opi/api.rb
|
@@ -81,29 +81,30 @@ files:
|
|
81
81
|
- lib/opi/loader.rb
|
82
82
|
- lib/opi/request.rb
|
83
83
|
- lib/opi/response.rb
|
84
|
+
- lib/opi/router.rb
|
84
85
|
- lib/opi/version.rb
|
85
86
|
homepage: http://github.com/moomerman/opi
|
86
87
|
licenses: []
|
87
88
|
metadata: {}
|
88
89
|
post_install_message:
|
89
90
|
rdoc_options:
|
90
|
-
- --inline-source
|
91
|
-
- --charset=UTF-8
|
91
|
+
- "--inline-source"
|
92
|
+
- "--charset=UTF-8"
|
92
93
|
require_paths:
|
93
94
|
- lib
|
94
95
|
required_ruby_version: !ruby/object:Gem::Requirement
|
95
96
|
requirements:
|
96
|
-
- -
|
97
|
+
- - ">="
|
97
98
|
- !ruby/object:Gem::Version
|
98
99
|
version: '0'
|
99
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
101
|
requirements:
|
101
|
-
- -
|
102
|
+
- - ">="
|
102
103
|
- !ruby/object:Gem::Version
|
103
104
|
version: '0'
|
104
105
|
requirements: []
|
105
106
|
rubyforge_project: opi
|
106
|
-
rubygems_version: 2.0.
|
107
|
+
rubygems_version: 2.2.0.rc.1
|
107
108
|
signing_key:
|
108
109
|
specification_version: 4
|
109
110
|
summary: The very opinionated API service library.
|