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.
@@ -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
@@ -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'}
@@ -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
+
@@ -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