marley 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|