opi 0.1 → 0.2
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 +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.
|