Introducing Snapi! Snapi-In API Generator
3/24/2014
by Gabe Koss
One of the challenges I have been working on at work surrounds rapid creation of liteweight web APIs. We develop functionality for a variety of different network security sensors and have to be able to quickly disclose to a variety of clients the capabilities of the individual systems.
In light of this I have started developing a Ruby Gem called Snapi
which we
have released under GPLv3.
The Github project is here. I also maintain a personal fork.
Sample Code
I want to expose the functionality of a Ninja
through a web API. Lets see
what that looks like in pure Ruby.
First a simple ninja:
class Ninja
def self.attack(params={})
"Ninja attacks #{params[:foe]}!"
end
def self.defend(params={})
"Ninja defends against #{params[:weapon]}!"
end
end
Next, I introduce the concept of a Snapi::Capability
. A Capability
is a
collection Snapi::Function
objects. These in turn accept n
Snapi::Arguments
defined for them.
I add the following to the Ninja
.
include Snapi::Capability
Technically at this point Snapi has created an API but since it has no
functions defined for it I'll add a couple Snapi::Function
definitions.
My complete class looks like this:
require 'snapi'
# These requirements should go away...
require 'sinatra'
require 'sinatra/contrib'
class Ninja
include Snapi::Capability
function :sneak
function :attack do |fn|
fn.argument :foe do |arg|
arg.required true
arg.type :string
end
end
function :defend do |fn|
fn.argument :weapon do |arg|
arg.required true
arg.type :string
end
end
def self.sneak(params={})
"Ninja sneaks..."
end
def self.attack(params={})
"Ninja attacks #{params[:foe]}!"
end
def self.defend(params={})
"Ninja defends against #{params[:weapon]}!"
end
end
Ruby API
This just got a bit more interesting! We have defined the ruby class
methods as Snapi
functions and our API is all built.
Note that the methods have an arity of one and expect to take a single hash argument. This makes them extremely flexible and adaptable so that additional functionality can be introduced without disrupting the external facing API.
Lets check out the basic functionality. This is still a bit rough!
Snapi.capabilities
#=> {:ninja=>Ninja}
Snapi.capability_hash
#=> {:ninja=>
# {:sneak=>{:return_type=>nil, :arguments=>[]},
# :attack=>
# {:return_type=>nil,
# :arguments=>[{:name=>:foe, :required=>true, :type=>:string}]},
# :defend=>
# {:return_type=>nil,
# :arguments=>[{:name=>:weapon, :required=>true, :type=>:string}]}}}
Snapi.capabilities[:ninja].run_function(:attack, {foe: "Samurai"})
#=> "Ninja attacks Samurai!"
Sinatra Extension
My favorite part of this is how easy it is to start
In the same file as the Ninja
class I'll add the following:
class Dojo < Sinatra::Base
register Snapi::SinatraExtension
run! if app_file == $0
end
When we run the file we get a nice little Sinatra webserver on localhost:4567
. Lets give it a whirl:
curl --data weapon=sword localhost:4567/ninja/defend
{"status":200,"data":{"raw_result":"Ninja defends against sword!"},"execution_time":0.000423193}
Future Improvements
This is far from a finished product. Snapi
has just started its life and has
many improvements to be made! In the (nearish) future I hope to see:
- Dynamic HTML form / view generation
- Dynamic CLI generation
- API Authentication
- Fix Gem Dependencies
- Better handling of list arguments
- Logging
- Cleaner error handling / meaningful feedback