scrumy.rb |
|
|---|---|
|
Scrumy Client is a Ruby REST client wrapper for Scrumy. Scrumy Client provides a simple client interface for retrieving Sprints, Stories, Tasks, and Scrumers, as well as some tools for generating useful information with those objects. |
|
|
The source code is available on Github. |
|
Dependencies |
|
|
We need JSON |
|
|
JSON responses are parsed and turned into the core Scrumy objects, key value pairs in the JSON hashes become instance variables in the objects. |
require 'json' |
|
Scrumy Client uses rest-client for conveniently retrieving REST resources and handling some of the HTTP at a higher level. |
require 'rest_client' |
|
We use ActiveSupport::Inflector to do
some of the metaprogramming magic and instantiate classes and create methods dynamically.
|
require 'active_support/inflector'
module Scrumy
class Client |
|
Every client request sets the |
attr_reader :url
|
|
|
def initialize(project, password)
@project, @password = project, password
end
|
|
This is the heart of the |
def method_missing(id, *args, &block) |
|
Figure out what kind of resource we’re trying to get. |
klass = Scrumy::Models.const_get(id.to_s.capitalize.singularize) |
|
Special case for handling id=:current – this really only applies to Sprint resources
but if other resources specified the |
if klass.current_url and args.first==:current
@url = format(klass.current_url, :current)
else |
|
TODO Figure out a better way of determining if the resource is singular or plural |
|
|
The only argument that resources ever take is an ID, so pass the first arg as the ID. |
@url = format((id.to_s =~ /s$/ ? klass.list_url : klass.show_url), args.first)
end |
|
Here we request the resource using the singular of the resource name as the root to extract from the returned JSON hash. |
response = get(@url, id.to_s.singularize)
|
|
Responses are of two types, either arrays of hashes or a single hash |
if response.kind_of? Array |
|
If it’s array collect a new array by constructing objects based on the resource name capitalized and singularized. |
response.collect do |obj|
klass.new(obj, self)
end
else |
|
Otherwise create a single new object of the correct type. |
klass.new(response, self)
end
end |
|
TODO This grammar should be better |
|
|
Currently subresources specify special sybmols in their name, either :project to get @project off the client, or :id to get the argument passed to the client. |
def format(url, id=nil)
url = url.gsub(':project', @project)
url = url.gsub(':id', id.to_s) if id
url
end
protected
|
|
For example if the resource returns |
def get(url, root)
begin |
|
Start by creating a new |
resource = RestClient::Resource.new(url, @project, @password)
|
|
|
resource.get {|response, request, result, &block|
case response.code
when 200 |
|
and on success parse the response |
json = JSON.parse(response.body) |
|
If it’s |
if json.kind_of?(Array) && root
json.collect{|item|
item[root]
}
else |
|
Otherwise just return the |
root ? json[root] : json
end
else
response.return!(request, result, &block)
end
}
rescue => e |
|
Rescue and reraise with the current |
raise "Problem fetching #{@url} because #{e.message}"
end
end
end
|
|
This is the abstract |
module Models
class Model
attr_reader :id |
|
When passed a hash the constructor will initialize the object with instance variables named after the keys in the hash. |
def initialize(args, client)
@client = client
args.each do |k,v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end |
|
This method missing provides a Ghost Method proxy to access or mutate any instance variable. |
def method_missing(id, *args, &block)
if id.to_s =~ /=$/
id = id.to_s.gsub(/=$/,'')
instance_variable_set("@#{id}", args.first)
else
instance_variable_get("@#{id}")
end
end
|
|
Adapter methods for the resource DSL, this provides the show, list, and current sub-resource defition methods. |
class << self |
|
For each :show, :list, :current |
[:show, :list, :current].each{|method| |
|
Create a method that sets a class instance variable to the URL argument |
send :define_method, method do |url|
instance_variable_set "@#{method.to_s}_url", url
end |
|
And create an accessor for that URL. |
send :define_method, "#{method.to_s}_url".to_sym do
instance_variable_get "@#{method.to_s}_url"
end
}
end
|
|
Only current Sprints are complete, so other models need to know how ot lazily load their children. |
|
|
Specifying a lazy_load key in a subclass defines a new instance method on that class that uses the client to fetch the right resource and set the appropriate instance variable correctly. |
def self.lazy_load(method)
define_method(method) {
client = instance_variable_get("@client")
ivar = instance_variable_get("@#{method}")
clss = Models.send :const_get, method.to_s.singularize.classify
root = method.to_s.singularize
|
|
First check if the instance variable is already set, but perhaps incorrectly as a Hash If so, then instantiate the instance variable as the correct type. |
if ivar.kind_of? Array
ivar.collect!{|single| clss.new(single[root], client)} if ivar and ivar.first.kind_of?Hash
elsif ivar
ivar = clss.new(ivar, client)
end
|
|
Return if already set, sort of minimal caching. |
return ivar if ivar |
|
Last resort, fetch from the rest client. |
ivar = client.send(method, instance_variable_get("@id"))
}
end
def self.helper(name, &block)
self.send :define_method, name do
instance_eval(&block)
end
end
end
end
end
|
|
This is entry point for the DSL that specifies resources |
def resource(name, &block) |
|
It creates a new class based on the resource name scoped tot he Scrumy module |
klass = Scrumy::Models.const_set(name.to_s.classify, Class.new(Scrumy::Models::Model)) |
|
Then executes the block on the class. The class provides several class methods for making instances behave correctly. |
klass.class_exec &block
end |
|
Loads in the default resources, see |
load('resources.rb') |