jsonrpc2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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