Since Perinci::CmdLine uses Riap behind the scenes (from getting the Rinci metadata to calling the function), it is possible to use a remote server as the Riap server, even when the server side is not Perl. Below are two examples. The first one uses piping (stdin/stdout) to access a Ruby program on the same server, and the second one uses TCP server written in Node.js. Note that the two programs are just quick-hacks and very ad-hoc, I haven’t actually developed any Riap libraries on those languages. Their main goal is to demonstrate the simplicity of the Riap::Simple protocol.
Ruby over pipe
Save this code to /some/path/to/riap_server.rb:
#!/usr/bin/env ruby require 'json' def _res(res) res[3] ||= {} res[3]['riap.v'] ||= 1.1 puts "j" + res.to_json $stdout.flush end while line = $stdin.gets do if line =~ /^j(.+)/ begin req = JSON.parse($1) rescue Exception => e _res [400, "Invalid JSON in Riap request: " + e.message] next end if !req['action'] _res [400, "Please specify 'action'"] next end if !req['uri'] _res [400, "Please specify 'uri'"] next end if req['action'] == 'call' if req['uri'] == '/cat_array' args = req['args'] || {} if (!args['a1']) _res [400, "Please specify a1"] next elsif (!args['a2']) _res [400, "Please specify a1"] next end _res [200,"OK",args['a1'] + args['a2']] next else _res [404, "Unknown uri"] next end elsif req['action'] == 'meta' if req['uri'] == '/cat_array' _res [200,"OK",{ "v" => 1.1, "summary" => "Concatenate two arrays together", "args" => { "a1" => { "summary" => "First array", "schema" => ["array"], "req" => true, }, "a2" => { "summary" => "Second array", "schema" => ["array"], "req" => true, }, }}] next else _res [404, "Unknown uri"] next end elsif req['action'] == 'info' if req['uri'] == '/cat_array' _res [200,"OK",{"type" => "function", "uri" => "/foo"}] next else _res [404, "Unknown uri"] next end else _res [400, "Invalid action"] next end else _res [400, "Invalid Riap request"] break end end
Now create our CLI program, let’s call it cat-array-ruby:
#!/usr/bin/env perl use Perinci::CmdLine::Classic; Perinci::CmdLine::Classic->new( url => "riap+pipe:/some/path/to/riap_server.rb////cat_array", )->run;
Let’s test the CLI program:
% cat-array-ruby --help cat-array-ruby - Concatenate two arrays together Usage -e --help (or -h, -?) -e --version (or -v) -e [options] Options --a1-json=s --a1-yaml=s --a1=s* --a2-json=s --a2-yaml=s --a2=s* --config-path=s --config-profile=s --debug --format-options=s --format=s --help, -h, -? --json --log-level=s --no-config --quiet --trace --verbose --version, -v For more complete help, use '--help --verbose'. % cat-array-ruby --a1-json '[1,2,3]' --a2-json '[4,5,6]' ┌─────────────────────────────┐ │ 1 2 3 4 5 6 │ └─────────────────────────────┘
All the other features you would normally get from a Perinci::CmdLine-based CLI application, like tab completion, output formatting, and so on works.
Node.js over TCP server
Save this code to riap_server.js:
function _res(s, res) { if (!res[3]) res[3] = {}; res[3]['riap.v'] = 1.1; s.write("j" + JSON.stringify(res) + "\015\012"); return; } var humanize = require('humanize'); var net = require('net'); var rl = require('readline'); var server = net.createServer(function(socket) { //'connection' listener console.log('client connected'); socket.on('end', function() { console.log('client disconnected'); }); var i = rl.createInterface(socket, socket); i.on('line', function (line) { match = line.match(/^j(.+)/) if (match) { // XXX error handling? var req = JSON.parse(match[1]); if (!req['action']) { _res(socket, [400, "Please specify action"]); } else if (!req['uri']) { _res(socket, [400, "Please specify uri"]); } else if (req['action'] == 'call') { var args = req['args'] || {} if (req['uri'] == '/humanize/filesize') { if (!args['size']) { _res(socket, [400, "Please specify size"]); } else { _res(socket, [200, "OK", humanize.filesize(args['size'])]); } } else { _res(socket, [404, "Unknown uri"]); } } else if (req['action'] == 'meta') { if (req['uri'] == '/humanize/filesize') { _res(socket, [200, "OK", { "v": 1.1, "summary": "Humanize file size", "args": { "size": { "schema": ["int"], "req": true, "pos": 0 } } }]); } else { _res(socket, [404, "Unknown uri"]); } } else if (req['action'] == 'info') { if (req['uri'] == '/humanize/filesize') { _res(socket, [200, "OK", {"uri":"/humanize/filesize", "type":"function"}]) } else { _res(socket, [404, "Unknown uri"]); } } else { _res(socket, [400, "Unknown action"]); } } else { _res(socket, [400, "Invalid Riap request"]); socket.destroy(); } }); }); server.listen(5000, function() { //'listening' listener console.log('server bound'); });
Install the humanize NPM module (if you doesn’t have the module) and run the server:
% npm install humanize % node riap_server.js server bound
Prepare our client, let’s call it humanize-filesize:
#!/usr/bin/env perl use Perinci::CmdLine::Classic; Perinci::CmdLine::Classic->new( url => "riap+tcp://localhost:5000/humanize/filesize", )->run;
Run our CLI:
% humanize-filesize --help humanize-filesize - Humanize file size Usage -e --help (or -h, -?) -e --version (or -v) -e [options] <size> Options --config-path=s --config-profile=s --debug --format-options=s --format=s --help, -h, -? --json --log-level=s --no-config --quiet --size=i* (=arg[0]) --trace --verbose --version, -v For more complete help, use '--help --verbose'. % humanize-filesize ERROR 400: Missing required argument(s): size % humanize-filesize 100200300 95.56 MB % humanize-filesize 100200300 --json [ 200, "OK", "95.56 MB", { "riap.v": 1.1 } ]
Note that in this blog post we are using Perinci::CmdLine::Classic instead of Perinci::CmdLine::Any because the default backend Perinci::CmdLine::Lite does not yet support the URL schemes riap+pipe:/ or riap+tcp://. This will be rectified sometime in the future.