marley 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Favicon.ico +0 -0
- data/README.rdoc +51 -0
- data/TODO +17 -0
- data/examples/empty.sqlite3 +0 -0
- data/examples/forum.css +3 -0
- data/examples/forum.js +23 -0
- data/examples/forum.rb +20 -0
- data/examples/forum.sql +42 -0
- data/examples/forum.sqlite3 +0 -0
- data/examples/forum_test.sqlite3 +0 -0
- data/examples/run.sh +14 -0
- data/lib/client/jamaica.css +270 -0
- data/lib/client/jamaica.js +353 -0
- data/lib/client/jamaica.rb +38 -0
- data/lib/client/jquery-1.6.2.js +8981 -0
- data/lib/client/jquery.form.js +814 -0
- data/lib/controllers.rb +69 -0
- data/lib/joint.rb +27 -0
- data/lib/joints/basic_menu_system.rb +54 -0
- data/lib/joints/basic_messaging.rb +88 -0
- data/lib/joints/basic_user.rb +51 -0
- data/lib/joints/tagged_messaging.rb +122 -0
- data/lib/joints/tagging.rb +60 -0
- data/lib/joints/user_based_navigation.rb +39 -0
- data/lib/marley.rb +148 -0
- data/lib/reggae.rb +110 -0
- data/lib/sequel_plugins.rb +141 -0
- data/lib/test_helpers.rb +52 -0
- data/marley-0.1.gem +0 -0
- data/marley.gemspec +16 -0
- data/reggae.ebnf +60 -0
- data/test/forum_tests.rb +356 -0
- metadata +116 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module Marley
|
3
|
+
module Joints
|
4
|
+
class UserBasedNavigation < Joint
|
5
|
+
LOGIN_FORM= [:instance,{:url => 'login',:description => 'Existing users please log in here:',:new_rec => true,:schema => [[:text,'name',RESTRICT_REQ],[:password,'password',RESTRICT_REQ]]}]
|
6
|
+
module ClassMethods
|
7
|
+
module User
|
8
|
+
def sections
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
module Resources
|
13
|
+
class MainMenu
|
14
|
+
attr_accessor :title,:name,:description, :items
|
15
|
+
def self.rest_get
|
16
|
+
new.to_json
|
17
|
+
end
|
18
|
+
def initialize
|
19
|
+
if $request[:user].new?
|
20
|
+
u=$request[:user].to_a
|
21
|
+
u[1].merge!({:description => 'If you don\'t already have an account, please create one here:'})
|
22
|
+
@title="Welcome to #{$request[:opts][:app_name]}"
|
23
|
+
@description='Login or signup here.'
|
24
|
+
@items=[LOGIN_FORM,u]
|
25
|
+
else
|
26
|
+
$request[:user].class.sections
|
27
|
+
end
|
28
|
+
end
|
29
|
+
def self.requires_user?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
def to_json
|
33
|
+
[:section,{:title => @title,:description => @description,:name => @name, :navigation => @items}]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/marley.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
require 'json/ext'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/auth/basic'
|
5
|
+
require 'rack/builder'
|
6
|
+
require 'sequel_plugins'
|
7
|
+
require 'controllers'
|
8
|
+
require 'reggae'
|
9
|
+
require 'logger'
|
10
|
+
Sequel.extension :inflector
|
11
|
+
|
12
|
+
Sequel::Model.plugin :rest_convenience
|
13
|
+
Sequel::Model.plugin :rest_authorization
|
14
|
+
Sequel::Model.plugin :validation_helpers
|
15
|
+
Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS.merge!(:presence => {:message => 'is required'})
|
16
|
+
Sequel::Model.plugin :timestamps, :create => :date_created, :update => :date_updated
|
17
|
+
|
18
|
+
log_fn='log/marley.log'
|
19
|
+
$log=Logger.new(File.exists?(log_fn) ? log_fn : $stdout)
|
20
|
+
|
21
|
+
|
22
|
+
module Marley #The main Marley namespace.
|
23
|
+
JOINT_DIRS=[File.expand_path("joints/",File.dirname(__FILE__)),"#{Dir.pwd}/joints"]
|
24
|
+
DEFAULT_OPTS={:http_auth => true,:app_name => 'Application',:port => 1620,:default_user_class => :User, :auth_class => :User,:default_resource => 'Menu', :server => 'thin'}
|
25
|
+
RESP_CODES={'get' => 200,'post' => 201,'put' => 204,'delete' => 204}
|
26
|
+
|
27
|
+
module Resources #All objects in the Resources namespace are exposed by the server.
|
28
|
+
end
|
29
|
+
require 'joint' #this needs to happen after Marley::Resources is defined
|
30
|
+
def self.config(opts=nil)
|
31
|
+
@marley_opts||=DEFAULT_OPTS
|
32
|
+
@marley_opts.merge!(opts) if opts
|
33
|
+
yield @marley_opts if block_given?
|
34
|
+
@marley_opts
|
35
|
+
end
|
36
|
+
def self.joint(joint_name, *opts)
|
37
|
+
joint_d=JOINT_DIRS.find {|d| File.exists?("#{d}/#{joint_name}.rb") }
|
38
|
+
require "#{joint_d}/#{joint_name}"
|
39
|
+
@marley_opts[:client] && @marley_opts[:client].joint(joint_d,joint_name)
|
40
|
+
joint=Marley::Joints.const_get(joint_name.camelize).new(*opts).smoke
|
41
|
+
end
|
42
|
+
def self.run(opts={})
|
43
|
+
@marley_opts||=DEFAULT_OPTS
|
44
|
+
marley_opts=@marley_opts.merge!(opts)
|
45
|
+
Rack::Handler.get(marley_opts[:server]).run(Rack::Builder.new {
|
46
|
+
use Rack::Reloader,0
|
47
|
+
use Rack::Static, :urls => [opts[:image_path]] if opts[:image_path]
|
48
|
+
run(Marley::Router.new(marley_opts))
|
49
|
+
}.to_app,{:Port => @marley_opts[:port]})
|
50
|
+
end
|
51
|
+
class Router #the default Marley router. Creates the $request object, locates the resource requested and calls either its controller's or its own rest verb method
|
52
|
+
def initialize(opts={},app=nil)
|
53
|
+
@opts=DEFAULT_OPTS.merge(opts)
|
54
|
+
end
|
55
|
+
def call(env)
|
56
|
+
request= Rack::Request.new(env)
|
57
|
+
@auth = Rack::Auth::Basic::Request.new(env)
|
58
|
+
$request={:request => request,:opts => @opts}
|
59
|
+
$request[:get_params]=Marley::Utils.hash_keys_to_syms(request.GET)
|
60
|
+
$request[:post_params]=Marley::Utils.hash_keys_to_syms(request.POST)
|
61
|
+
$request[:content_type]=request.xhr? ? 'application/json' : env['HTTP_ACCEPT'].to_s.sub(/,.*/,'')
|
62
|
+
$request[:content_type]='text/html' unless $request[:content_type] > ''
|
63
|
+
$request[:content_type]='application/json' if env['rack.test']==true #there has to be a better way to do this...
|
64
|
+
if @opts[:http_auth]
|
65
|
+
if (@auth.provided? && @auth.basic? && @auth.credentials)
|
66
|
+
$request[:user]=Resources.const_get(@opts[:auth_class]).authenticate(@auth.credentials)
|
67
|
+
raise AuthenticationError unless $request[:user]
|
68
|
+
else
|
69
|
+
$request[:user]=Resources.const_get(@opts[:default_user_class]).new
|
70
|
+
end
|
71
|
+
end
|
72
|
+
$request[:path]=request.path.sub(/\/\/+/,'/').split('/')[1..-1]
|
73
|
+
verb=request.request_method.downcase
|
74
|
+
verb=$request[:post_params].delete(:_method).match(/^(put|delete)$/i)[1] rescue verb
|
75
|
+
$request[:verb]="rest_#{verb}"
|
76
|
+
rn=$request[:path] ? $request[:path][0].camelize : @opts[:default_resource]
|
77
|
+
raise RoutingError unless Resources.constants.include?(rn)
|
78
|
+
@resource=Resources.const_get(rn)
|
79
|
+
raise AuthenticationError if @opts[:http_auth] && @resource.respond_to?('requires_user?') && @resource.requires_user? && $request[:user].new?
|
80
|
+
@controller=@resource.respond_to?($request[:verb]) ? @resource : @resource.controller
|
81
|
+
json=@controller.send($request[:verb]).to_json
|
82
|
+
html=@opts[:client] ? @opts[:client].to_s(json) : json
|
83
|
+
resp_code=RESP_CODES[verb]
|
84
|
+
headers||={'Content-Type' => "#{$request[:content_type]}; charset=utf-8"}
|
85
|
+
[resp_code,headers,$request[:content_type].match(/json/) ? json : html]
|
86
|
+
rescue Sequel::ValidationFailed
|
87
|
+
ValidationError.new($!.errors).response
|
88
|
+
rescue
|
89
|
+
($!.class.new.respond_to?(:response) ? $!.class : MarleyError).new.response
|
90
|
+
ensure
|
91
|
+
$log.info $request.merge({:request => nil,:user => $request[:user] ? $request[:user].name : nil})
|
92
|
+
end
|
93
|
+
end
|
94
|
+
class MarleyError < StandardError
|
95
|
+
class << self
|
96
|
+
attr_accessor :resp_code,:headers,:description,:details
|
97
|
+
end
|
98
|
+
@resp_code=500
|
99
|
+
def initialize
|
100
|
+
self.class.details=self.backtrace
|
101
|
+
end
|
102
|
+
def log_error
|
103
|
+
$log.fatal("#$!.message}\n#{$!.backtrace}")
|
104
|
+
end
|
105
|
+
def response
|
106
|
+
log_error
|
107
|
+
json=[:error,{:error_type => self.class.name.underscore.sub(/_error$/,'').sub(/^marley\//,''),:description => self.class.description, :error_details => self.class.details}].to_json
|
108
|
+
self.class.headers||={'Content-Type' => "#{$request[:content_type]}; charset=utf-8"}
|
109
|
+
[self.class.resp_code,self.class.headers,json]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
class ValidationError < MarleyError
|
113
|
+
@resp_code=400
|
114
|
+
def initialize(errors)
|
115
|
+
self.class.details=errors
|
116
|
+
end
|
117
|
+
def log_error
|
118
|
+
$log.error(self.class.details)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
class AuthenticationError < MarleyError
|
122
|
+
@resp_code=401
|
123
|
+
@headers={'WWW-Authenticate' => %(Basic realm="Application")}
|
124
|
+
def log_error
|
125
|
+
$log.error("Authentication failed for #{@auth.credentials[0]}") if (@auth && @auth.provided? && @auth.basic? && @auth.credentials)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
class AuthorizationError < MarleyError
|
129
|
+
@resp_code=403
|
130
|
+
@description='You are not authorized for this operation'
|
131
|
+
def log_error
|
132
|
+
$log.error("Authorizationt Error:#{self.message}")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
class RoutingError < MarleyError
|
136
|
+
@resp_code=404
|
137
|
+
@description='Not Found'
|
138
|
+
def log_error
|
139
|
+
$log.fatal("path:#{$request[:path]}\n msg:#{$!.message}\n backtrace:#{$!.backtrace}")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
module Utils
|
143
|
+
def self.hash_keys_to_syms(hsh)
|
144
|
+
hsh.inject({}) {|h,(k,v)| h[k.to_sym]= v.class==Hash ? hash_keys_to_syms(v) : v;h }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
at_exit {Marley.run if ARGV[0]=='run'}
|
data/lib/reggae.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
|
2
|
+
module Marley
|
3
|
+
class Reggae < Array
|
4
|
+
class << self
|
5
|
+
attr_accessor :valid_properties
|
6
|
+
def mk_prop_methods
|
7
|
+
@valid_properties && @valid_properties.each do |meth|
|
8
|
+
define_method(meth) {properties[meth].respond_to?(:to_resource) ? properties[meth].to_resource : properties[meth]}
|
9
|
+
define_method(:"#{meth}=") {|val|properties[meth]=val}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
def get_resource(*args)
|
13
|
+
self.new(*args).to_resource
|
14
|
+
end
|
15
|
+
end
|
16
|
+
def initialize(*args)
|
17
|
+
super
|
18
|
+
self.class.mk_prop_methods
|
19
|
+
end
|
20
|
+
def resource_type
|
21
|
+
[String, Symbol].include?(self[0].class) ? self[0].to_s : nil
|
22
|
+
end
|
23
|
+
def is_resource?
|
24
|
+
! resource_type.nil?
|
25
|
+
end
|
26
|
+
def properties
|
27
|
+
self[1].class==Hash ? Utils.hash_keys_to_syms(self[1]) : nil
|
28
|
+
end
|
29
|
+
def contents
|
30
|
+
is_resource? ? Reggae.new(self[2 .. -1]) : nil
|
31
|
+
end
|
32
|
+
def contents=(*args)
|
33
|
+
self[2]=*args
|
34
|
+
while length>3;delete_at -1;end
|
35
|
+
end
|
36
|
+
def [](*args)
|
37
|
+
super.class==Array ? Reggae.new(super).to_resource : super
|
38
|
+
end
|
39
|
+
def to_resource
|
40
|
+
is_resource? ? Marley.const_get("Reggae#{resource_type.camelize}".to_sym).new(self) : self
|
41
|
+
end
|
42
|
+
def find_instances(rn,instances=Reggae.new([]))
|
43
|
+
if self.class==ReggaeInstance && self.name.to_s==rn
|
44
|
+
instances << self
|
45
|
+
else
|
46
|
+
(is_resource? ? contents : self).each {|a| a && Reggae.new(a).to_resource.find_instances(rn,instances)}
|
47
|
+
end
|
48
|
+
instances
|
49
|
+
end
|
50
|
+
end
|
51
|
+
class ReggaeResource < Reggae
|
52
|
+
def initialize(*args)
|
53
|
+
super
|
54
|
+
unshift self.class.to_s.sub(/.*Reggae/,'').underscore.to_sym unless is_resource?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
class ReggaeSection < ReggaeResource
|
58
|
+
self.valid_properties=[:title,:description]
|
59
|
+
def navigation
|
60
|
+
properties[:navigation].map{|n|Reggae.get_resource(n)}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
class ReggaeLink < ReggaeResource
|
64
|
+
self.valid_properties=[:title,:description,:url]
|
65
|
+
end
|
66
|
+
class ReggaeInstance < ReggaeResource
|
67
|
+
self.valid_properties=[:name,:new_rec,:search,:url,:get_actions,:delete_action]
|
68
|
+
def schema
|
69
|
+
ReggaeSchema.new(self.properties[:schema])
|
70
|
+
end
|
71
|
+
def to_params
|
72
|
+
resource_name=name
|
73
|
+
schema.inject({}) do |params,spec|
|
74
|
+
s=ReggaeColSpec.new(spec)
|
75
|
+
params["#{resource_name}[#{s.col_name}]"]=s.col_value unless (s.col_restrictions & RESTRICT_RO > 0)
|
76
|
+
params
|
77
|
+
end
|
78
|
+
end
|
79
|
+
def instance_action_url(action_name)
|
80
|
+
"#{url}#{action_name}" if get_actions.include?(action_name.to_s)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
class ReggaeInstanceList < ReggaeResource
|
84
|
+
self.valid_properties=[:name,:description,:get_actions,:delete_action,:items]
|
85
|
+
def schema
|
86
|
+
ReggaeSchema.new(self.properties[:schema])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
class ReggaeMsg < ReggaeResource
|
90
|
+
self.valid_properties=[:title,:description]
|
91
|
+
end
|
92
|
+
class ReggaeError < ReggaeResource
|
93
|
+
self.valid_properties=[:error_type,:description,:error_details]
|
94
|
+
end
|
95
|
+
class ReggaeSchema < Array
|
96
|
+
def [](i)
|
97
|
+
if i.class==Fixnum
|
98
|
+
ReggaeColSpec.new(super)
|
99
|
+
else
|
100
|
+
self[find_index {|cs|ReggaeColSpec.new(cs).col_name==i.to_s}]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
class ReggaeColSpec < Array
|
105
|
+
['col_type','col_name','col_restrictions', 'col_value'].each_with_index do |prop_name, i|
|
106
|
+
define_method(prop_name.to_sym) {self[i]}
|
107
|
+
define_method(:"#{prop_name}=") {|val|self[i]=val}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
RESTRICT_HIDE=1
|
2
|
+
RESTRICT_RO=2
|
3
|
+
RESTRICT_REQ=4
|
4
|
+
TYPE_INDEX=0
|
5
|
+
NAME_INDEX=1
|
6
|
+
RESTRICTIONS_INDEX=2
|
7
|
+
module Sequel::Plugins::RestConvenience
|
8
|
+
module ClassMethods
|
9
|
+
def controller
|
10
|
+
Marley::ModelController.new(self)
|
11
|
+
end
|
12
|
+
def resource_name
|
13
|
+
self.name.sub(/.*::/,'').underscore
|
14
|
+
end
|
15
|
+
def reggae_link(action=nil)
|
16
|
+
[:link,{:url => "/#{self.resource_name}/#{action}",:title => "#{action.humanize} #{self.resource_name.humanize}".strip}]
|
17
|
+
end
|
18
|
+
def list(params=nil)
|
19
|
+
user=$request[:user]
|
20
|
+
if user.respond_to?(otm=self.resource_name.pluralize)
|
21
|
+
if user.method(otm).arity==0
|
22
|
+
if (relationship=user.send(otm)).respond_to?(:filter)
|
23
|
+
relationship.filter($request[:get_params][resource_name.to_sym])
|
24
|
+
else
|
25
|
+
user.send(otm)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
user.send(otm,$request[:get_params][resource_name.to_sym])
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise Marley::AuthorizationError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
def autocomplete(input_content)
|
35
|
+
filter(:name.like("#{input_content.strip}%")).map {|rec| [rec.id, rec.name]}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
module InstanceMethods
|
39
|
+
def get_actions; [];end
|
40
|
+
def edit; self; end
|
41
|
+
def rest_cols
|
42
|
+
columns.reject do |c|
|
43
|
+
if new?
|
44
|
+
c.to_s.match(/(^id$)|(_type$)|(date_(created|updated))/)
|
45
|
+
else
|
46
|
+
c.to_s.match(/_type$/)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
def hidden_cols
|
51
|
+
columns.select {|c| c.to_s.match(/(_id$)/)}
|
52
|
+
end
|
53
|
+
def write_cols
|
54
|
+
rest_cols.reject {|c| c.to_s.match(/(^id$)|(date_(created|updated))/)}
|
55
|
+
end
|
56
|
+
def required_cols;[];end
|
57
|
+
def rest_schema
|
58
|
+
rest_cols.map do |col_name|
|
59
|
+
db_spec=db_schema.to_hash[col_name]
|
60
|
+
col_type=db_spec ? db_spec[:db_type].downcase : col_name
|
61
|
+
restrictions=0
|
62
|
+
restrictions|=RESTRICT_HIDE if hidden_cols.include?(col_name)
|
63
|
+
restrictions|=RESTRICT_RO unless write_cols.include?(col_name)
|
64
|
+
restrictions|=RESTRICT_REQ if required_cols.include?(col_name) || (db_spec && !db_spec[:allow_null])
|
65
|
+
[col_type, col_name, restrictions,send(col_name)]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
def to_s
|
69
|
+
respond_to?('name') ? name : id.to_s
|
70
|
+
end
|
71
|
+
def to_a
|
72
|
+
a=Marley::ReggaeInstance.new([ {:name => self.class.resource_name,:url => url ,:new_rec => self.new?,:schema => rest_schema,:get_actions => get_actions}])
|
73
|
+
if respond_to?(:rest_associations) && ! new?
|
74
|
+
a.contents=rest_associations.map do |assoc|
|
75
|
+
(assoc.class==Symbol ? send(assoc) : assoc).map{|instance| instance.to_a}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
a
|
79
|
+
end
|
80
|
+
def to_json
|
81
|
+
to_a.to_json
|
82
|
+
end
|
83
|
+
def url(action=nil)
|
84
|
+
"/#{self.class.resource_name}/#{self[:id]}/#{action}".sub('//','/')
|
85
|
+
end
|
86
|
+
def reggae_link(action=nil)
|
87
|
+
[:link,{:url => url,:title => "#{action.humanize}"}]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
module Sequel::Plugins::RestAuthorization
|
92
|
+
module ClassMethods
|
93
|
+
attr_accessor :owner_col, :allowed_get_methods
|
94
|
+
def inherited(c)
|
95
|
+
super
|
96
|
+
c.owner_col=@owner_col
|
97
|
+
c.allowed_get_methods=@allowed_get_methods
|
98
|
+
end
|
99
|
+
def requires_user?(verb=nil,meth=nil);true;end
|
100
|
+
def authorize(meth)
|
101
|
+
if respond_to?(auth_type="authorize_#{$request[:verb]}")
|
102
|
+
send(auth_type,meth)
|
103
|
+
else
|
104
|
+
case $request[:verb]
|
105
|
+
when 'rest_put','rest_delete'
|
106
|
+
false
|
107
|
+
when 'rest_post'
|
108
|
+
new($request[:post_params][resource_name.to_sym]||{}).current_user_role=='owner' && meth.nil?
|
109
|
+
when 'rest_get'
|
110
|
+
(@allowed_get_methods || ['section','list','new']).include?(meth)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
module InstanceMethods
|
116
|
+
def after_initialize
|
117
|
+
send("#{self.class.owner_col}=",$request[:user][:id]) if self.class.owner_col && new?
|
118
|
+
end
|
119
|
+
def requires_user?(verb=nil,meth=nil);true;end
|
120
|
+
def authorize(meth)
|
121
|
+
if respond_to?(auth_type="authorize_#{$request[:verb]}")
|
122
|
+
send(auth_type,meth)
|
123
|
+
else
|
124
|
+
current_user_role=='owner'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
def current_user_role
|
128
|
+
"owner" if owners.include?($request[:user])
|
129
|
+
end
|
130
|
+
def owners
|
131
|
+
if self.class.to_s.match(/User$/)||self.class.superclass.to_s.match(/User$/)
|
132
|
+
[self]
|
133
|
+
elsif @owner_col
|
134
|
+
[User[send(@owner_col)]]
|
135
|
+
else
|
136
|
+
self.class.association_reflections.select {|k,v| v[:type]==:many_to_one}.map {|a| self.send(a[0]) && self.send(a[0]).owners}.flatten.compact
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
data/lib/test_helpers.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require "rack/test"
|
2
|
+
require 'reggae'
|
3
|
+
module Marley
|
4
|
+
#simple mocking framework; could be expanded to a general use client by adding display code.
|
5
|
+
class TestClient
|
6
|
+
CRUD2REST={'create' => 'post','read' => 'get','update' => 'put', 'del' => 'delete'}
|
7
|
+
DEFAULT_OPTS={:url => nil,:root_url => nil, :resource_name => nil, :instance_id => nil, :method => nil, :extention =>nil, :auth => nil, :code => nil, :debug => nil}
|
8
|
+
include Rack::Test::Methods
|
9
|
+
attr_reader :opts
|
10
|
+
def app
|
11
|
+
Marley::Router.new
|
12
|
+
end
|
13
|
+
def initialize(opts)
|
14
|
+
@opts=DEFAULT_OPTS.merge(opts)
|
15
|
+
end
|
16
|
+
def make_url(opts=nil)
|
17
|
+
opts||=@opts
|
18
|
+
opts[:url] || '/' + [:root_url, :resource_name, :instance_id, :method].map {|k| opts[k]}.compact.join('/') + opts[:extention].to_s
|
19
|
+
end
|
20
|
+
def process(verb,params={},opts={})
|
21
|
+
opts||={}
|
22
|
+
opts=@opts.merge(opts)
|
23
|
+
expected_code=opts[:code] || RESP_CODES[verb]
|
24
|
+
if opts[:debug]
|
25
|
+
p opts
|
26
|
+
p "#{verb} to: '#{make_url(opts)}'"
|
27
|
+
p params
|
28
|
+
p opts[:auth]
|
29
|
+
end
|
30
|
+
authorize opts[:auth][0],opts[:auth][1] if opts[:auth]
|
31
|
+
header 'Authorization',nil unless opts[:auth] #clear auth from previous requests
|
32
|
+
send(verb,make_url(opts),params)
|
33
|
+
p last_response.status if opts[:debug]
|
34
|
+
p expected_code if opts[:debug]
|
35
|
+
return false unless (expected_code || RESP_CODES[method])==last_response.status
|
36
|
+
Reggae.get_resource(JSON.parse(last_response.body)) rescue last_response.body
|
37
|
+
end
|
38
|
+
['create','read','update','del'].each do |op|
|
39
|
+
define_method op.to_sym, Proc.new { |params,opts|
|
40
|
+
process(CRUD2REST[op],params,opts)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
DEFAULT_OPTS.keys.each do |opt|
|
44
|
+
define_method opt, Proc.new {
|
45
|
+
@opts[opt]
|
46
|
+
}
|
47
|
+
define_method "#{opt}=", Proc.new { |val|
|
48
|
+
@opts[opt]=val
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|