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,137 @@
1
+ require 'cgi'
2
+ module JSONRPC2
3
+ # HTML output helpers for browseable API interface
4
+ module HTML
5
+ module_function
6
+ # Wrap body in basic bootstrap template using cdn
7
+ def html5(title, body, options={})
8
+ request = options[:request]
9
+ [
10
+ <<-HTML5
11
+ <!DOCTYPE html><html>
12
+ <head>
13
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
14
+ <title>#{title}</title>
15
+ #{options[:head]}
16
+ <link rel="stylesheet" href="//current.bootstrapcdn.com/bootstrap-v204/css/bootstrap-combined.min.css">
17
+ <script src="//current.bootstrapcdn.com/bootstrap-v204/js/bootstrap.min.js"></script>
18
+ <style>
19
+ body {
20
+ padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
21
+ }
22
+ </style>
23
+ </head>
24
+ <body> <div class="navbar navbar-fixed-top">
25
+ <div class="navbar-inner">
26
+ <div class="container">
27
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
28
+ <span class="icon-bar"></span>
29
+ <span class="icon-bar"></span>
30
+ <span class="icon-bar"></span>
31
+ </a>
32
+ <a class="brand" href="#">JSON-RPC Interface</a>
33
+ <div class="nav-collapse">
34
+ <ul class="nav">
35
+ <li class="#{['', '/'].include?(request.path_info) ? 'active' : ''}"><a href="#{request.script_name}/">API Overview</a></li>
36
+ </ul>
37
+ </div><!--/.nav-collapse -->
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="container">#{body}</div>
43
+ </body>
44
+ </html>
45
+ HTML5
46
+ ]
47
+ end
48
+
49
+ # Process browser request for #interface
50
+ # @param [JSONRPC2::Interface] interface Interface being accessed
51
+ # @param [Rack::Request] request Request being processed
52
+ # @return [Rack::Response]
53
+ def call(interface, request)
54
+ #require 'pp'; pp interface.about
55
+
56
+ if interface.auth_with.kind_of?(JSONRPC2::HttpAuth)
57
+ response = catch(:rack_response) do
58
+ interface.auth_with.check(request.env, {}); nil
59
+ end
60
+ return response if response
61
+ end
62
+
63
+ case request.path_info
64
+ when /^\/([a-zA-Z_0-9]+)/
65
+ method = $1
66
+ if info = interface.about_method(method)
67
+ if json = request.POST['__json__']
68
+ begin
69
+ data = JSON.parse(json)
70
+ result = interface.new(request.env).dispatch(data)
71
+ rescue => e
72
+ result = e.class.name + ": " + e.message
73
+ end
74
+ end
75
+ [200, {'Content-Type' => 'text/html'}, html5(method,describe(interface, request, info, :result => result), :request => request) ]
76
+ else
77
+ [404, {'Content-Type' => 'text/html'}, html5("Method not found", "<h1>No such method</h1>", :request => request)]
78
+ end
79
+ else
80
+ body = RedCloth.new(interface.to_textile).to_html.gsub(/\<h3\>(.*?)\<\/h3\>/, '<h3><a href="'+request.script_name+'/\1">\1</a></h3>')
81
+ [200, {'Content-Type' => 'text/html'},
82
+ html5('Interface: '+interface.name.to_s, body, :request => request)]
83
+ end
84
+ end
85
+ # Returns HTML page describing method
86
+ def describe interface, request, info, options = {}
87
+ params = {}
88
+ if info[:params]
89
+ info[:params].each do |param|
90
+ params[param[:name]] = case param[:type]
91
+ when 'String'
92
+ ""
93
+ when 'Boolean', 'false'
94
+ false
95
+ when 'true'
96
+ true
97
+ when 'null'
98
+ nil
99
+ when 'Number', 'Integer'
100
+ 0
101
+ when /^Array/
102
+ []
103
+ else
104
+ {}
105
+ end
106
+ end
107
+ end
108
+ <<-EOS
109
+ <h1>Method Info: #{info[:name]}</h1>
110
+ #{RedCloth.new(interface.method_to_textile(info)).to_html}
111
+
112
+ <hr>
113
+
114
+ <h2>Test method</h2>
115
+ <div class="row">
116
+ <div class="span6">
117
+ <form method="POST" action="#{request.script_name}/#{info[:name]}">
118
+ <textarea name="__json__" cols="60" rows="8" class="span6">
119
+ #{CGI.escapeHTML((request.POST['__json__'] || JSON.pretty_unparse({'jsonrpc'=>'2.0', 'method' => info[:name], 'id' => 1, 'params' => params})).strip)}
120
+ </textarea>
121
+ <div class="form-actions">
122
+ <input type="submit" class="btn btn-primary" value="Call Method">
123
+ </div>
124
+ </form>
125
+ </div>
126
+ <div class="span6">
127
+ <h3>Result</h3>
128
+ <xmp>
129
+ #{options[:result]}
130
+ </xmp>
131
+ </div>
132
+ </div>
133
+
134
+ EOS
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,298 @@
1
+ require 'jsonrpc2'
2
+ require 'jsonrpc2/accept'
3
+ require 'jsonrpc2/textile'
4
+ require 'jsonrpc2/auth'
5
+ require 'jsonrpc2/html'
6
+ require 'jsonrpc2/types'
7
+ require 'json'
8
+ require 'base64'
9
+
10
+ module JSONRPC2
11
+ # Authentication failed error - only used if transport level authentication isn't.
12
+ # e.g. BasicAuth returns a 401 HTTP response, rather than throw this.
13
+ class AuthFail < RuntimeError; end
14
+
15
+ # API error - thrown when an error is detected, e.g. params of wrong type.
16
+ class APIFail < RuntimeError; end
17
+
18
+ # Base class for JSONRPC2 interface
19
+ class Interface
20
+ class << self
21
+
22
+ # @!group Authentication
23
+
24
+ # Get/set authenticator object for API interface - see {JSONRPC2::Auth} and {JSONRPC2::BasicAuth}
25
+ #
26
+ # @param [#check] args An object that responds to check(environment, json_call_data)
27
+ # @return [#check, nil] Currently set object or nil
28
+ def auth_with *args
29
+ if args.empty?
30
+ return @auth_with
31
+ else
32
+ @auth_with = args[0]
33
+ end
34
+ end
35
+
36
+ # @!endgroup
37
+
38
+ # @!group Rack-related
39
+
40
+ # Rack compatible call handler
41
+ #
42
+ # @param [Hash] environment Rack environment hash
43
+ # @return [Array<Fixnum, Hash<String,String>, Array<String>>] Rack-compatible response
44
+ def call(environment)
45
+ request = Rack::Request.new(environment)
46
+ catch :rack_response do
47
+ case JSONRPC2::HTTPUtils.which(environment['HTTP_ACCEPT'], %w[text/html application/json-rpc application/json])
48
+ when 'text/html', nil
49
+ JSONRPC2::HTML.call(self, request)
50
+ when 'application/json-rpc', 'application/json'
51
+ environment['rack.input'].rewind
52
+ data = JSON.parse(environment['rack.input'].read)
53
+ self.new(environment).rack_dispatch(data)
54
+ else
55
+ [406, {'Content-Type' => 'text/html'},
56
+ ["<!DOCTYPE html><html><head><title>Media type mismatch</title></head><body>I am unable to acquiesce to your request</body></html>"]]
57
+ end
58
+ end
59
+ end
60
+
61
+ # @!endgroup
62
+
63
+ end
64
+
65
+ # Create new interface object
66
+ #
67
+ # @param [Hash] env Rack environment
68
+ def initialize(env)
69
+ @env = env
70
+ end
71
+ # Internal
72
+ def rack_dispatch(rpcData)
73
+ catch(:rack_response) do
74
+ json = dispatch(rpcData)
75
+ [200, {'Content-Type' => 'application/json-rpc'}, [json]]
76
+ end
77
+ end
78
+
79
+ protected
80
+ # Dispatch call to api method(s)
81
+ #
82
+ # @param [Hash,Array] rpc_data Array of calls or Hash containing one call
83
+ # @return [Hash,Array] Depends on input, but either a hash result or an array of results corresponding to calls.
84
+ def dispatch(rpc_data)
85
+ case rpc_data
86
+ when Array
87
+ rpc_data.map { |rpc| dispatch_single(rpc) }.to_json
88
+ else
89
+ dispatch_single(rpc_data).to_json
90
+ end
91
+ end
92
+ # JSON result helper
93
+ def response_ok(id, result)
94
+ { 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
95
+ end
96
+ # JSON error helper
97
+ def response_error(code, message, data)
98
+ { 'jsonrpc' => '2.0', 'error' => { 'code' => code, 'message' => message, 'data' => data }, 'id' => @id }
99
+ end
100
+ # Check call validity and authentication & make a single method call
101
+ #
102
+ # @param [Hash] rpc JSON-RPC-2 call
103
+ def dispatch_single(rpc)
104
+ unless rpc.has_key?('id') && rpc.has_key?('method') && rpc['jsonrpc'].eql?('2.0')
105
+ @id = nil
106
+ return response_error(-32600, 'Invalid request', nil)
107
+ end
108
+ @id = rpc['id']
109
+ @method = rpc['method']
110
+ @rpc = rpc
111
+
112
+ begin
113
+ if self.class.auth_with
114
+ self.class.auth_with.check(@env, rpc) or raise AuthFail, "Invalid credentials"
115
+ end
116
+
117
+ call(rpc['method'], rpc['id'], rpc['params'])
118
+ rescue AuthFail => e
119
+ response_error(-32000, "AuthFail: #{e.class}: #{e.message}", {}) # XXX: Change me
120
+ rescue APIFail => e
121
+ response_error(-32000, "APIFail: #{e.class}: #{e.message}", {}) # XXX: Change me
122
+ rescue Exception => e
123
+ response_error(-32000, "#{e.class}: #{e.message}", e.backtrace) # XXX: Change me
124
+ end
125
+ end
126
+ # List API methods
127
+ #
128
+ # @return [Array] List of api method names
129
+ def api_methods
130
+ public_methods(false).map(&:to_s) - ['rack_dispatch']
131
+ end
132
+
133
+ # Call method, checking param and return types
134
+ #
135
+ # @param [String] method Method name
136
+ # @param [Integer] id Method call ID - for response
137
+ # @param [Hash] params Method parameters
138
+ # @return [Hash] JSON response
139
+ def call(method, id, params)
140
+ if api_methods.include?(method)
141
+ begin
142
+ Types.valid_params?(self.class, method, params)
143
+ rescue Exception => e
144
+ return response_error(-32602, "Invalid params - #{e.message}", {})
145
+ end
146
+
147
+ if self.method(method).arity.zero?
148
+ result = send(method)
149
+ else
150
+ result = send(method, params)
151
+ end
152
+
153
+ begin
154
+ Types.valid_result?(self.class, method, result)
155
+ rescue Exception => e
156
+ return response_error(-32602, "Invalid result - #{e.message}", {})
157
+ end
158
+
159
+ response_ok(id, result)
160
+ else
161
+ response_error(-32601, "Unknown method `#{method.inspect}'", {})
162
+ end
163
+ end
164
+
165
+ class << self
166
+ # Store parameter in internal hash when building API
167
+ def ___append_param name, type, options
168
+ @params ||= []
169
+ unless options.has_key?(:required)
170
+ options[:required] = true
171
+ end
172
+ @params << options.merge({ :name => name, :type => type })
173
+ end
174
+ private :___append_param
175
+
176
+ # @!group DSL
177
+
178
+ # Define a named parameter of type #type for next method
179
+ #
180
+ # @param [String] name parameter name
181
+ # @param [String] type description of type see {Types}
182
+ def param name, type, desc = nil, options = nil
183
+ if options.nil? && desc.is_a?(Hash)
184
+ options, desc = desc, nil
185
+ end
186
+ options ||= {}
187
+ options[:desc] = desc if desc.is_a?(String)
188
+
189
+ ___append_param name, type, options
190
+ end
191
+
192
+ # Define an optional parameter for next method
193
+ def optional name, type, desc = nil, options = nil
194
+ if options.nil? && desc.is_a?(Hash)
195
+ options, desc = desc, nil
196
+ end
197
+ options ||= {}
198
+ options[:desc] = desc if desc.is_a?(String)
199
+
200
+ ___append_param(name, type, options.merge(:required => false))
201
+ end
202
+
203
+ # Define type of return value for next method
204
+ def result type, desc = nil
205
+ @result = { :type => type, :desc => desc }
206
+ end
207
+
208
+ # Set description for next method
209
+ def desc str
210
+ @desc = str
211
+ end
212
+
213
+ # Add an example for next method
214
+ def example desc, code
215
+ @examples ||= []
216
+ @examples << { :desc => desc, :code => code }
217
+ end
218
+
219
+ # Define a custom type
220
+ def type name, *fields
221
+ @types ||= {}
222
+ type = JsonObjectType.new(name, fields)
223
+
224
+ if block_given?
225
+ yield(type)
226
+ end
227
+
228
+ @types[name] = type
229
+ end
230
+
231
+ # Group methods
232
+ def section name, summary=nil
233
+ @sections ||= []
234
+ @sections << {:name => name, :summary => summary}
235
+
236
+ @current_section = name
237
+ if block_given?
238
+ yield
239
+ @current_section = nil
240
+ end
241
+ end
242
+
243
+ # Exclude next method from documentation
244
+ def nodoc
245
+ @nodoc = true
246
+ end
247
+
248
+ # Set interface title
249
+ def title str = nil
250
+ @title = str if str
251
+ end
252
+
253
+ # Sets introduction for interface
254
+ def introduction str = nil
255
+ @introduction = str if str
256
+ end
257
+
258
+ # @!endgroup
259
+
260
+ # Catch methods added to class & store documentation
261
+ def method_added(name)
262
+ return if self == JSONRPC2::Interface
263
+ @about ||= {}
264
+ method = {}
265
+ method[:params] = @params if @params
266
+ method[:returns] = @result if @result
267
+ method[:desc] = @desc if @desc
268
+ method[:examples] = @examples if @examples
269
+
270
+ if method.empty?
271
+ if public_methods(false).include?(name)
272
+ unless @nodoc
273
+ #logger.info("#{name} has no API documentation... :(")
274
+ end
275
+ else
276
+ #logger.debug("#{name} isn't public - so no API")
277
+ end
278
+ else
279
+ method[:name] = name
280
+ method[:section] = @current_section
281
+ method[:index] = @about.size
282
+ @about[name.to_s] = method
283
+ end
284
+
285
+ @result = nil
286
+ @params = nil
287
+ @desc = nil
288
+ @examples = nil
289
+ @nodoc = false
290
+ end
291
+ private :method_added
292
+ attr_reader :about, :types
293
+
294
+ end
295
+
296
+ extend JSONRPC2::TextileEmitter
297
+ end
298
+ end
@@ -0,0 +1,131 @@
1
+ require 'redcloth'
2
+
3
+ module JSONRPC2
4
+ # Textile documentation output functions
5
+ module TextileEmitter
6
+ # Returns interface description in textile
7
+ def to_textile
8
+ return nil if @about.nil? or @about.empty?
9
+ str = ""
10
+ if @title
11
+ str << "h1. #{@title}\n"
12
+ else
13
+ str << "h1. #{name}\n"
14
+ end
15
+ if @introduction
16
+ str << "\nh2. Introduction\n\n#{@introduction}\n"
17
+ end
18
+
19
+ unless @types.nil? or @types.empty?
20
+ str << "\nh2. Types\n"
21
+ @types.sort_by { |k,v| k }.each do |k,type|
22
+ str << "\nh5. #{k} type\n"
23
+
24
+ str << "\n|_. Field |_. Type |_. Required? |_. Description |"
25
+ type.fields.each do |field|
26
+ str << "\n| @#{field[:name]}@ | @#{field[:type]}@ | #{field[:required] ? 'Yes' : 'No'} | #{field[:desc]} |"
27
+ end
28
+ str << "\n"
29
+ end
30
+ end
31
+
32
+ @sections.each do |section|
33
+ str << "\nh2. #{section[:name]}\n"
34
+ if section[:summary]
35
+ str << "\n#{section[:summary]}\n"
36
+ end
37
+
38
+ str += to_textile_group(section).to_s
39
+ end
40
+ miscfn = to_textile_group({:name => nil})
41
+ if miscfn
42
+ str << "\nh2. Misc functions\n"
43
+ str << miscfn
44
+ end
45
+ str
46
+ end
47
+ # Returns method description in textile
48
+ def method_to_textile(info)
49
+ str = ''
50
+ str << "\nh3. #{info[:name]}\n"
51
+ str << "\n#{info[:desc]}\n" if info[:desc]
52
+ str << "\nh5. Params\n"
53
+ if info[:params].nil?
54
+ str << "\n* _None_\n"
55
+ elsif info[:params].is_a?(Array)
56
+ str << "\n|_. Name |_. Type |_. Required |_. Description |\n"
57
+ info[:params].each do |param|
58
+ str << "| @#{param[:name]}@ | @#{param[:type]}@ | #{param[:required] ? 'Yes' : 'No'} | #{param[:desc]} |\n"
59
+ end
60
+ end
61
+
62
+ if res = info[:returns]
63
+ str << "\nh5. Result\n"
64
+ str << "\n* @#{res[:type]}@"
65
+ str << " - #{res[:desc]}" if res[:desc]
66
+ str << "\n"
67
+ else
68
+ str << "\nh5. Result\n"
69
+ str << "\n* @null@"
70
+ end
71
+
72
+ if examples = info[:examples]
73
+ str << "\nh5. Sample usage\n"
74
+
75
+ nice_json = lambda do |data|
76
+ JSON.pretty_unparse(data).gsub(/\n\n+/,"\n").gsub(/[{]\s+[}]/m, '{ }').gsub(/\[\s+\]/m, '[ ]')
77
+ end
78
+ examples.each do |ex|
79
+ str << "\n#{ex[:desc]}\n"
80
+ code = ex[:code]
81
+ if code.is_a?(String)
82
+ str << "\nbc. #{ex[:code]}\n"
83
+ elsif code.is_a?(Hash) && code.has_key?(:params) && (code.has_key?(:result) || code.has_key?(:error))
84
+
85
+ str << "\nbc. "
86
+ if code[:result] # ie. we expect success
87
+ unless JSONRPC2::Types.valid_params?(self, info[:name], code[:params])
88
+ raise "Invalid example params for #{info[:name]} / #{ex[:desc]}"
89
+ end
90
+ end
91
+ input = { 'jsonrpc' => 2.0, 'method' => info[:name], 'params' => code[:params], 'id' => 0 }
92
+ str << "--> #{nice_json.call(input)}\n"
93
+
94
+ if code[:error]
95
+ error = { 'jsonrpc' => 2.0, 'error' => code[:error], 'id' => 0 }
96
+ str << "<-- #{nice_json.call(error)}\n"
97
+ elsif code[:result]
98
+ unless JSONRPC2::Types.valid_result?(self, info[:name], code[:result])
99
+ raise "Invalid result example for #{info[:name]} / #{ex[:desc]}"
100
+ end
101
+
102
+ result = { 'jsonrpc' => 2.0, 'result' => code[:result], 'id' => 0 }
103
+ str << "<-- #{nice_json.call(result)}\n"
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ str
110
+ end
111
+ # Gets method docs for method #name
112
+ def about_method(name)
113
+ @about[name.to_s]
114
+ end
115
+ # Returns documentation #section contents in textile
116
+ def to_textile_group(section)
117
+ list = @about.values.select { |info| info[:section] == section[:name] }
118
+
119
+ return nil if list.empty?
120
+
121
+ str = ''
122
+
123
+ list.sort_by { |info| info[:index] }.each do |info|
124
+ str << method_to_textile(info)
125
+ end
126
+
127
+ str
128
+ end
129
+
130
+ end
131
+ end