jsonrpc2 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsonrpc2.gemspec
4
+ gemspec
@@ -0,0 +1,176 @@
1
+ # JSON-RPC2 Ruby Server
2
+
3
+ A Rack compatible, documenting JSON-RPC 2 DSL/server implementation for ruby.
4
+
5
+ ## Features
6
+
7
+ * Inline documentation
8
+ * Type checking for parameters and return values
9
+ * Authentication support - including HTTP Basic Authentication
10
+ * Rack mountable
11
+ * Interactive browser based API testing
12
+
13
+ ## Example
14
+
15
+ class Calculator < JSONRPC2::Interface
16
+ title "JSON-RPC2 Calculator"
17
+ introduction "This interface allows basic maths calculations via JSON-RPC2"
18
+ auth_with JSONRPC2::BasicAuth.new({'apiuser' => 'secretword'})
19
+
20
+ section 'Simple Ops' do
21
+ desc 'Multiply two numbers'
22
+ param 'a', 'Number', 'First number'
23
+ param 'b', 'Number', 'Second number'
24
+ result 'Number', 'a * b'
25
+ def mul args
26
+ args['a'] * args['b']
27
+ end
28
+
29
+ desc 'Add numbers'
30
+ param 'a', 'Number', 'First number'
31
+ param 'b', 'Number', 'Second number'
32
+ optional 'c', 'Number', 'Third number'
33
+ example 'Calculate 1 + 1', :params => { 'a' => 1, 'b' => 1}, :result => 2
34
+ result 'Number', 'a + b + c'
35
+ def sum args
36
+ val = args['a'] + args['b']
37
+ val += args['c'] if args['c']
38
+ val
39
+ end
40
+ end
41
+ end
42
+
43
+ To run example:
44
+
45
+ $ gem install shotgun # unless it's already installed
46
+ $ shotgun example/config.ru
47
+
48
+ Browse API and test it via a web browser at http://localhost:9393/
49
+
50
+
51
+ ## Inline documentation
52
+
53
+ Use built in helper methods to declare complex types and function
54
+ parameters before defining method calls.
55
+
56
+ ### Custom type definitions
57
+
58
+ e.g.
59
+
60
+ type "Address" do |t|
61
+ t.string "street", "Street name"
62
+ t.string "city", "City"
63
+ ...
64
+ end
65
+
66
+ type "Person" do |t|
67
+ t.string "name", "Person's name"
68
+ t.number "age", "Person's age"
69
+ t.boolean "is_member", "Is person a member of our club?"
70
+ t.optional do
71
+ t.field "address", "Address", "Address of person"
72
+ end
73
+ end
74
+
75
+ #### type "Name", &block
76
+
77
+ > Declare a JSON object type with named keys and value types
78
+
79
+ #### field "Name", "Type", "Description"
80
+ #### string "Name", "Description"
81
+ #### number "Name", "Description"
82
+ #### integer "Name", "Description"
83
+ #### boolean "Name", "Description"
84
+
85
+ > Describes the members of a JSON object - fields can be of any known type (see {JSONRPC2::Types} for details).
86
+
87
+ #### optional &block
88
+ #### required &block
89
+
90
+ > Use blocks to specify whether an object field is required or optional
91
+
92
+ ---
93
+
94
+ ### Method annotations
95
+
96
+ e.g.
97
+
98
+ desc 'Add numbers'
99
+ param 'a', 'Number', 'First number'
100
+ param 'b', 'Number', 'Second number'
101
+ optional 'c', 'Number', 'Third number'
102
+ example 'Calculate 1 + 1', :params => { 'a' => 1, 'b' => 1}, :result => 2
103
+ result 'Number', 'a + b + c'
104
+ def sum args
105
+ val = args['a'] + args['b']
106
+ val += args['c'] if args['c']
107
+ val
108
+ end
109
+
110
+ #### desc "Description of method"
111
+
112
+ > Short description of what the method does
113
+
114
+ #### param "Name", "Type", "Description"
115
+
116
+ or
117
+
118
+ #### optional "Name", "Type", "Description"
119
+
120
+ > Description of a named parameter for the method, including type and purpose (see {JSONRPC2::Types} for type details)
121
+
122
+ #### result "Type", "Description"
123
+
124
+ > Type and description of return value for method (see {JSONRPC2::Types} for type details)
125
+
126
+ #### example "Description", "Detail"
127
+
128
+ > Describe example usage
129
+
130
+ #### example "Description", { :params => { ... }, :result => value }
131
+
132
+ > Describe example usage and valid result (NB: values specified for both params and result are checked against the method type descriptions and generating docs will throw an error if the values are invalid).
133
+
134
+ #### example "Description", { :params => { ... }, :error => value }
135
+
136
+ > Describe example usage and sample error (NB: values specified for params are checked against the method type descriptions and generating docs throws an error if the values are invalid).
137
+
138
+ #### nodoc
139
+
140
+ > Don't include next method in documentation
141
+
142
+ ---
143
+
144
+ ### Interface annotations
145
+
146
+ e.g.
147
+
148
+ title "Calculator interface"
149
+ introduction "Very simple calculator interface"
150
+
151
+ section "Entry points" do
152
+ ...
153
+ end
154
+
155
+ #### title "Title"
156
+
157
+ > Set title for interface
158
+
159
+ #### introduction "Introduction/description of interface"
160
+
161
+ > Add a basic introduction for the API
162
+
163
+ #### section "Name", &block
164
+
165
+ > Group methods into logical sections for documentation purposes
166
+
167
+ ### Authentication
168
+
169
+ #### auth_with Authenticator
170
+
171
+ e.g.
172
+ auth_with JSONRPC2::BasicAuth.new({'apiuser' => 'secretword'})
173
+
174
+ > Specify authentication method that should be used to verify the access credentials before printing. See {JSONRPC2::BasicAuth} for examples/info.
175
+
176
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,34 @@
1
+ $: << File.join(File.dirname(__FILE__),'../lib')
2
+ require 'jsonrpc2/interface'
3
+
4
+ class ::Object::Calculator < JSONRPC2::Interface
5
+ title "JSON-RPC2 Calculator"
6
+ introduction "This interface allows basic maths calculations via JSON-RPC2"
7
+ auth_with JSONRPC2::BasicAuth.new({'user' => 'secretword'})
8
+
9
+ section 'Simple Ops' do
10
+ desc 'Multiply two numbers'
11
+ param 'a', 'Number', 'a'
12
+ param 'b', 'Number', 'b'
13
+ result 'Number', 'a * b'
14
+ def mul args
15
+ args['a'] * args['b']
16
+ end
17
+
18
+ desc 'Add numbers'
19
+ example "Calculate 1 + 1 = 2", :params => { 'a' => 1, 'b' => 1}, :result => 2
20
+
21
+ param 'a', 'Number', 'First number'
22
+ param 'b', 'Number', 'Second number'
23
+ optional 'c', 'Number', 'Third number'
24
+ result 'Number', 'a + b + c'
25
+ def sum args
26
+ val = args['a'] + args['b']
27
+ val += args['c'] if args['c']
28
+ val
29
+ end
30
+ end
31
+ end
32
+
33
+ run Calculator
34
+
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/jsonrpc2/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Geoff Youngs"]
6
+ gem.email = ["git@intersect-uk.co.uk"]
7
+ gem.description = <<-EOD
8
+ JSON-RPC2 server DSL - allows APIs to be created as mountable Rack applications
9
+ with inline documentation, authentication and type checking.
10
+
11
+ e.g.
12
+
13
+ class Calculator < JSONRPC2::Interface
14
+ title "JSON-RPC2 Calculator"
15
+ introduction "This interface allows basic maths calculations via JSON-RPC2"
16
+ auth_with JSONRPC2::BasicAuth.new({'user' => 'secretword'})
17
+
18
+ section 'Simple Ops' do
19
+ desc 'Multiply two numbers'
20
+ param 'a', 'Number', 'a'
21
+ param 'b', 'Number', 'b'
22
+ result 'Number', 'a * b'
23
+ def mul args
24
+ args['a'] * args['b']
25
+ end
26
+
27
+ desc 'Add numbers'
28
+ example "Calculate 1 + 1 = 2", :params => { 'a' => 1, 'b' => 1}, :result => 2
29
+
30
+ param 'a', 'Number', 'First number'
31
+ param 'b', 'Number', 'Second number'
32
+ optional 'c', 'Number', 'Third number'
33
+ result 'Number', 'a + b + c'
34
+ def sum args
35
+ val = args['a'] + args['b']
36
+ val += args['c'] if args['c']
37
+ val
38
+ end
39
+ end
40
+ end
41
+
42
+ EOD
43
+ gem.summary = %q{JSON-RPC2 server DSL}
44
+ gem.homepage = "http://github.com/geoffyoungs/jsonrpc2"
45
+
46
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
47
+ gem.files = `git ls-files`.split("\n")
48
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
49
+ gem.name = "jsonrpc2"
50
+ gem.require_paths = ["lib"]
51
+ gem.version = JSONRPC2::VERSION
52
+ gem.add_dependency("httpclient")
53
+ gem.add_dependency("json")
54
+ gem.add_development_dependency("RedCloth")
55
+ end
@@ -0,0 +1,4 @@
1
+ require "jsonrpc2/version"
2
+
3
+ module JSONRPC2
4
+ end
@@ -0,0 +1,67 @@
1
+ # Utils for parsing HTTP fields
2
+ module JSONRPC2::HTTPUtils
3
+ module_function
4
+ # Converts */* -> /^.*?/.*?$/
5
+ # text/* -> /^text\/.*?$/
6
+ # text/html -> /^text\/html$/
7
+ #
8
+ # @param [String] type Media type descriptor
9
+ # @return [Regexp] Regular expression that matches type
10
+ def type_to_regex type
11
+ case type
12
+ when /[*]/
13
+ Regexp.new("^#{type.split(/[*]/).map { |bit| Regexp.quote(bit) }.join('.*?')}$")
14
+ else
15
+ Regexp.new("^#{Regexp.quote(type)}$")
16
+ end
17
+ end
18
+
19
+ # Parses the HTTP Accept field and returns a sorted list of prefered
20
+ # types
21
+ #
22
+ # @param field
23
+ def parse_accept field, regex = false
24
+ index = -1
25
+ list = field.split(/,\s*/).map do |media|
26
+ index += 1
27
+ case media
28
+ when /;/
29
+ media, param_str = *media.split(/\s*;\s*(?=q\s*=)/,2)
30
+ params = param_str.to_s.split(/\s*;\s*/).inject({}) { |hash, str|
31
+ k,v = *str.strip.split(/=/).map(&:strip)
32
+ hash.merge(k => v)
33
+ }
34
+ { :q => (params['q'] || 1.0).to_f, :media => media, :index => index }
35
+ else
36
+ { :q => 1.0, :media => media, :index => index }
37
+ end
38
+ end.sort_by { |option| [-1 * option[:q], option[:media].scan(/[*]/).size, option[:index]] }
39
+
40
+ final = {}
41
+ list.each do |item|
42
+ q = item[:q]
43
+ final[q] ||= []
44
+ final[q].push(regex ? type_to_regex(item[:media]) : item[:media])
45
+ end
46
+
47
+ final.sort_by { |k,v| -1 * k }
48
+ end
49
+
50
+ # Selects the clients preferred media/mime type based on Accept header
51
+ #
52
+ # @param [String] http_client_accepts HTTP Accepts header
53
+ # @param [Array<String>] options Media types available
54
+ def which http_client_accepts, options
55
+ return nil unless http_client_accepts
56
+
57
+ parse_accept(http_client_accepts, true).each do |preference, types|
58
+ types.each do |type|
59
+ options.each do |option|
60
+ return option if type.match(option)
61
+ end
62
+ end
63
+ end
64
+
65
+ nil
66
+ end
67
+ end
@@ -0,0 +1,66 @@
1
+ module JSONRPC2
2
+
3
+ # @abstract Base authentication class
4
+ class Auth
5
+ # Validate an API request
6
+ #
7
+ #
8
+ def check(env, rpc)
9
+ true
10
+ end
11
+ end
12
+
13
+ # @abstract Base class for http-based authentication methods, e.g.
14
+ # {BasicAuth}
15
+ class HttpAuth < Auth
16
+ end
17
+
18
+ # HTTP Basic authentication implementation
19
+ class BasicAuth < HttpAuth
20
+ # Create a BasicAuth object
21
+ #
22
+ # @param [Hash<String,String>] users containing containing usernames and passwords
23
+ # @yield [user, pass] Username and password to authenticate
24
+ # @yieldreturn [Boolean] True if credentials are approved
25
+ def initialize(users=nil, &block)
26
+ @users, @lookup = users, block
27
+ end
28
+
29
+ # Checks that the client is authorised to access the API
30
+ #
31
+ # @param [Hash,Rack::Request] env Rack environment hash
32
+ # @param [Hash] rpc JSON-RPC2 call content
33
+ # @return [true] Returns true or throws :rack_response, [ 401, ... ]
34
+ def check(env, rpc)
35
+ valid?(env) or
36
+ throw(:rack_response, [401, {
37
+ 'Content-Type' => 'text/html',
38
+ 'WWW-Authenticate' => 'Basic realm="API"'
39
+ }, ["<html><head/><body>Authentication Required</body></html>"]])
40
+ end
41
+
42
+ def valid?(env)
43
+ auth = env['HTTP_AUTHORIZATION']
44
+
45
+ return false unless auth
46
+
47
+ m = /Basic\s+([A-Za-z0-9+\/]+=*)/.match(auth)
48
+ user, pass = Base64.decode64(m[1]).split(/:/, 2)
49
+ user_valid?(user, pass)
50
+ end
51
+
52
+ # Checks users hash and then the block given to the constructor to
53
+ # verify username / password.
54
+ def user_valid?(user, pass)
55
+ if @users && @users.respond_to?(:[])
56
+ if expected = @users[user]
57
+ return pass == expected
58
+ end
59
+ end
60
+ if @lookup
61
+ return @lookup.call(user, pass)
62
+ end
63
+ false
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,59 @@
1
+ require 'httpclient'
2
+ require 'json'
3
+
4
+ module JSONRPC2
5
+ # JSON RPC client error
6
+ class RemoteError < RuntimeError
7
+ end
8
+
9
+ # Simple JSONRPC client
10
+ class Client
11
+ # Create client object
12
+ #
13
+ # @param [String] uri Create client object
14
+ # @param [Hash] options Global options
15
+ def initialize(uri, options = {})
16
+ @uri = uri
17
+ @client = HTTPClient.new
18
+ @options = options
19
+ @id = 0
20
+ end
21
+
22
+ # Call method with named arguments
23
+ # @param [String] method Remote method name
24
+ # @param [Hash<String,Value>] args Hash of named arguments for function
25
+ # @param [Hash<String,String>] options Additional parameters
26
+ # @return Method call result
27
+ # @raise [RemoteError] Error thrown by API
28
+ # @raise Transport/Network/HTTP errors
29
+ def call(method, args = {}, options = {}, &block)
30
+ headers = { 'Content-Type' => 'application/json-rpc' }
31
+
32
+ # Merge one level of hashes - ie. merge :headers
33
+ options = @options.merge(options) { |key,v1,v2| v2 = v1.merge(v2) if v1.class == v2.class && v1.is_a?(Hash); v2 }
34
+
35
+ if options[:headers]
36
+ headers = headers.merge(options[:headers])
37
+ end
38
+
39
+ if options[:auth]
40
+ @client.set_auth(@uri, options[:auth][:username], options[:auth][:password])
41
+ end
42
+ result = @client.post(@uri,
43
+ { 'method' => method, 'params' => args, 'jsonrpc' => '2.0', 'id' => (@id+=1) }.to_json,
44
+ headers)
45
+ if result.contenttype =~ /^application\/json/
46
+ body = result.body
47
+ body = body.content if body.respond_to?(:content) #
48
+ data = JSON.parse body
49
+ if data.has_key?('result')
50
+ return data['result']
51
+ else
52
+ raise RemoteError, data['error']['message']
53
+ end
54
+ else
55
+ raise result.body
56
+ end
57
+ end
58
+ end
59
+ end