pericmd 042: Using functions from other languages

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.

Leave a comment