Web Service DSL
Weasel Diesel is a DSL to describe and document your web API.
To get you going quickly, see the generator for sinatra apps. The wd_sinatra gem allows you to generate the structure for a sinatra app using Weasel Diesel and with lots of goodies. Updating is trivial since the core features are provided by this library and the wd_sinatra gem.
You can also check out this Sinatra-based example application that you can fork and use as a base for your application.
DSL examples:
describe_service "/hello_world" do |service|
service.formats :json
service.http_verb :get # default verb, can be ommitted.
service.disable_auth # on by default
# INPUT
service.param.string :name, :default => 'World', :doc => "The name of the person to greet."
# OUTPUT
service.response do |response|
response.object do |obj|
obj.string :message, :doc => "The greeting message sent back. Defaults to 'World'"
obj.datetime :at, :doc => "The timestamp of when the message was dispatched"
end
end
# DOCUMENTATION
service.documentation do |doc|
doc.overall "This service provides a simple hello world implementation example."
doc.example "<code>curl -I 'http://localhost:9292/hello_world?name=Matt'</code>"
end
# ACTION/IMPLEMENTATION (specific to the sinatra app example, can
# instead be set to call a controller action)
service.implementation do
{:message => "Hello #{params[:name]}", :at => Time.now}.to_json
end
end
Or a more complex example using XML:
SpecOptions = ['RSpec', 'Bacon'] # usually pulled from a model
describe_service "/wsdsl/test.xml" do |service|
service.formats :xml, :json
service.http_verb :get
# INPUT
service.params do |p|
p.string :framework, :in => SpecOptions, :null => false, :required => true
p.datetime :timestamp,
:default => Time.now,
:doc => "The test framework used, could be one of the two following: #{SpecOptions.join(", ")}."
p.string :alpha, :in => ['a', 'b', 'c']
p.string :version,
:null => false,
:doc => "The version of the framework to use."
p.integer :num, :minvalue => 42
p.namespace :user do |user|
user.integer :id, :required => :true
end
end
# OUTPUT
# the response contains a list of player creation ratings each object in the list
service.response do |response|
response.element(:name => "player_creation_ratings") do |e|
e.attribute :id => :integer, :doc => "id doc"
e.attribute :is_accepted => :boolean, :doc => "is accepted doc"
e.attribute :name => :string, :doc => "name doc"
e.array :name => 'player_creation_rating', :type => 'PlayerCreationRating' do |a|
a.attribute :comments => :string, :doc => "comments doc"
a.attribute :player_id => :integer, :doc => "player_id doc"
a.attribute :rating => :integer, :doc => "rating doc"
a.attribute :username => :string, :doc => "username doc"
end
end
end
# DOCUMENTATION
service.documentation do |doc|
# doc.overall <markdown description text>
doc.overall <<-DOC
This is a test service used to test the framework.
DOC
# doc.example <markdown text>
doc.example <<-DOC
The most common way to use this service looks like that:
http://example.com/wsdsl/test.xml?framework=rspec&version=2.0.0
DOC
end
end
INPUT DSL
As shown in the two examples above, input parameters can be:
- optional or required
- namespaced
- typed
- marked as not being null if passed
- set to have a value defined in a list
- set to have a min value
- set to have a min length
- set to have a max value
- set to have a max length
- documented
Most of these settings are used to verify the input requests.
Supported defined types:
- integer
- float, decimal
- string
- boolean
- array (comma delimited string)
- binary, file
Note regarding required vs optional params.
You can't set a required param to be :null => true
, if you do so, the
setting will be ignored since all required params have to be present.
If you set an optional param to be :null => false
, the verification
will only fail if the param was present in the request but the passed
value is nil. You might want to use that setting if you have an optional
param that, by definition isn't required but, if passed has to not be
null.
Validation and other param options
You can set many rules to define an input parameter. Here is a quick overview of the available param options, check the specs for more examples. Options can be combined.
required
by default the defined optional input parameters are optional. However their presence can be required by using this flag. (Setting:null => true
will be ignored if the paramter is required) Example:service.param.string :id, :required => true
in
oroptions
limits the range of the possible values being passed. Example:service.param.string :skills, :options %w{ruby scala clojure}
default
sets a value for your in case you don't pass one. Example:service.param.datetime :timestamp, :default => Time.now.iso8601
min_value
forces the param value to be equal or greater than the option's value. Example: `service.param.integer :age, :min_value => 21max_value
forces the param value to be equal or less than the options's value. Example: `service.param.integer :votes, :max_value => 7min_length
forces the length of the param value to be equal or greater than the option's value. Example:service.param.string :name, :min_length => 2
max_length
forces the length of the param value to be equal or lesser than the options's value. Example:service.param.string :name, :max_length => 251
null
in the case of an optional parameter, if the parameter is being passed, the value can't be nil or empty.doc
document the param.
Namespaced/nested object
Input parameters can be defined nested/namespaced. This is particuliarly frequent when using Rails for instance.
service.params do |param|
param.string :framework,
:in => ['RSpec', 'Bacon'],
:required => true,
:doc => "The test framework used, could be one of the two following: #{WeaselDieselSpecOptions.join(", ")}."
param.datetime :timestamp, :default => Time.now
param.string :alpha, :in => ['a', 'b', 'c']
param.string :version, :null => false, :doc => "The version of the framework to use."
param.integer :num, :min_value => 42, :max_value => 1000, :doc => "The number to test"
param.string :name, :min_length => 5, :max_length => 25
end
service.params.namespace :user do |user|
user.integer :id, :required => :true
user.string :sex, :in => %Q{female, male}
user.boolean :mailing_list, :default => true, :doc => "is the user subscribed to the ML?"
user.array :skills, :in => %w{ruby js cooking}
end
service.params.namespace :attachment, :null => true do ||
.string :url, :required => true
end
Here is the same type of input but this time using a JSON jargon,
namespace
and object
are aliases and can therefore can be used based
on how the input type.
# INPUT using 1.9 hash syntax
service.params do |param|
param.integer :playlist_id,
doc: "The ID of the playlist to which the track belongs.",
required: true
param.object :track do |track|
track.string :title,
doc: "The title of the track.",
required: true
track.string :album_title,
doc: "The title of the album to which the track belongs.",
required: true
track.string :artist_name,
doc: "The name of the track's artist.",
required: true
track.string :rdio_id,
doc: "The Rdio ID of the track.",
required: true
end
end
OUTPUT DSL
JSON API example
Consider the following JSON response:
{ people: [
{
id : 1,
online : false,
created_at : 123123123123,
team : {
id : 1231,
score : 123.32
}
},
{
id : 2,
online : true,
created_at : 123123123123,
team: {
id : 1233,
score : 1.32
}
},
] }
It would be described as follows:
describe_service "/json_list" do |service|
service.formats :json
service.response do |response|
response.array :people do |node|
node.integer :id
node.boolean :online
node.datetime :created_at
node.object :team do |team|
team.integer :id
team.float :score, :null => true
end
end
end
end
Nodes/elements can also use some meta-attributes including:
key
: refers to an attribute name that is key to this objecttype
: refers to the type of object described, valuable when using JSON across OO based apps.
JSON response validation can be done using an optional module as shown in (spec/json_response_verification_spec.rb)[https://github.com/mattetti/Weasel-Diesel/blob/master/spec/json_response_verification_spec.rb]. The goal of this module is to help automate API testing by validating the data structure of the returned object.
Another simple examples:
Actual output:
{"organization": {"name": "Example"}}
Output DSL:
describe_service "example" do |service|
service.formats :json
service.response do |response|
response.object :organization do |node|
node.string :name
end
end
end
Actual output:
{"name": "Example"}
Output DSL:
describe_service "example" do |service|
service.formats :json
service.response do |response|
response.object do |node|
node.string :name
end
end
end
Documentation generation
$ weasel_diesel generate_doc <SOURCE_PATH> <DESTINATION_PATH>
To generate documentation for the APIs you created in the api folder. The source path is the location of your ruby files. The destination is optional, 'doc' is the default.
Test Suite & Dependencies
The test suite requires rspec
, rack
, and sinatra
gems.
Copyright
Copyright (c) 2012 Matt Aimonetti. See LICENSE for further details.