marley 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -43,36 +43,11 @@ Marley activates both of them for Sequel::Model. This will soon change to an op
43
43
  One of the things that the RestConvenience Sequel plugin does is add a #controller method to affected models. This method instantiates and returns a ModelController object for the model in question. At initialization, the Controller parses the request path to determine the model instances to which the request refers.
44
44
 
45
45
 
46
- ==Joints
46
+ :include: rdoc/joints.rdoc
47
47
 
48
- "Joints" are pre-packaged resource sets that can be included in a Marley application. A joint can be added to the current Resources by running Marley.joint('_joint_name_'). The Marley.joint method will then do the following:
49
-
50
- * Find a file named '_joint_name_' in the 'joints/' directory.
51
- * Require that file.
52
- * run its #smoke method.
53
-
54
- The Joint#smoke method looks for 3 modules inside the joint's namespace: Resources, ClassMethods, and InstanceMethods.
55
-
56
- * if the Resources module exists, Joint#smoke will copy all of its constants to Marley::Resources.
57
- * If the ClassMethods module exists, Joint#smoke will cycle through the modules within it, and extend objects in Marley::Resources with the same name.
58
- * If the InstanceMethods module exists, Joint#smoke will cycle through the modules within it, and call their #append_features with the corresponding objects in Marley::Resources with the same name.
59
-
60
- For now, there are 5 joints included in the Marley distribution:
61
-
62
- * Basic User
63
- * Basic Messaging
64
- * Basic Menu System
65
- * Tagging
66
- * Tagged Messaging
67
-
68
- With a bit of configuration, these comprise the example forum application, which is in turn the targt for the test suite.
69
-
70
- ==Reggae
71
-
72
- The server and client use a JSON based data representation format "Reggae." Reggae.ebnf describes the format in canonical form, and reggae.rb contains a Reggae parser and generator.
48
+ :include: rdoc/reggae.rdoc
73
49
 
74
50
  ==Jamaica
75
51
 
76
52
  The default Marley client is "Jamaica", which consists of JS/CSS for browsers. It sucks right now and I'm hoping somebody takes it over as a sub-project, but it does work - at least on FF.
77
53
 
78
-
data/Rakefile CHANGED
@@ -9,3 +9,6 @@ Rake::TestTask.new(:test) do |t|
9
9
  t.test_files=FileList['test/*_tests.rb']
10
10
  end
11
11
 
12
+ Rake::TestTask.new(:unit) do |t|
13
+ t.test_files=FileList['test/*_unit.rb']
14
+ end
data/TODO CHANGED
@@ -1,17 +1,19 @@
1
1
 
2
2
 
3
- - improve testing coverage
4
- - improve documentation - esp API
5
-
3
+ - complete switchover to tdoc
4
+ - joint dependencies
5
+ - rename client to sth like output
6
+ - move all output there
7
+ - fix JS
6
8
  - ditch Sequel dependency
9
+ - ORM as a Joint??
10
+ - automate sequel plugin loading in options
7
11
 
8
- - automate sequel plugin loading in options
9
-
10
- - better logging
12
+ -----------------
11
13
 
12
- - hints for resources/columns
13
14
  - better filtering options for posts
14
-
15
- ??? expose validation reflections to views
15
+ - better logging
16
+ - implement instance_list
17
+ - expose validation reflections to views
16
18
  ??? caching protocol for autocompleters/validation options
17
19
 
@@ -1,34 +1,17 @@
1
1
 
2
- module Sequel::Plugins::RestSection
3
- SECTION_PROPS='name','title','description','navigation'
4
- module ClassMethods
5
- SECTION_PROPS.each {|p| attr_accessor :"section_#{p}"}
6
- def section
7
- if SECTION_PROPS.find {|p| send(:"section_#{p}").to_s > ''}
8
- Marley::ReggaeSection.new(SECTION_PROPS.inject({}) do |props,p|
9
- prop=send(:"section_#{p}")
10
- props[p.to_sym]=prop.class==Hash ? prop[$request[:user].class] : prop
11
- props
12
- end)
13
- end
14
- end
15
- end
16
- end
17
2
  module Marley
18
3
  module Joints
19
4
  class BasicMenuSystem < Joint
5
+ RestSection=Marley::Utils.rest_opts_mod('section',['name','title','description','navigation'],lambda {$request[:user].class})
20
6
  def smoke
21
7
  super
22
- Sequel::Model.plugin :rest_section
8
+ Sequel::Model.extend RestSection
23
9
  end
24
10
  module Resources
25
11
  class Menu
26
- class <<self
27
- attr_accessor :sections
28
- end
29
- include Sequel::Plugins::RestSection::ClassMethods
12
+ include RestSection
30
13
  def self.rest_get
31
- new.section
14
+ new.rest_section
32
15
  end
33
16
  def self.requires_user?
34
17
  ! $request[:path].to_a.empty?
@@ -44,8 +27,8 @@ module Marley
44
27
  else
45
28
  @section_title = "#{$request[:opts][:app_name]} Main Menu"
46
29
  @section_description="Welcome to #{$request[:opts][:app_name]}, #{$request[:user].name}"
47
- @section_navigation=(self.class.sections || (MR.constants - [self.class.to_s.sub(/.*::/,'').to_sym])).map do |rn|
48
- if (resource=MR.const_get(rn)).respond_to?(:section) && (s=resource.section) && s.title
30
+ @section_navigation=(MR.constants - [self.class.to_s.sub(/.*::/,'').to_sym]).map do |rn|
31
+ if (resource=MR.const_get(rn)).respond_to?(:rest_section) && (s=resource.rest_section) && s.title
49
32
  [:link,{:title => s.title, :description =>s.description, :url => "#{resource.resource_name}/section" }]
50
33
  end
51
34
  end.compact
@@ -17,7 +17,7 @@ module Marley
17
17
  super << [:text,:author,RESTRICT_RO,author.to_s]
18
18
  end
19
19
  def authorize_rest_get(meth)
20
- current_user_role && (meth.nil? || get_actions.include?(meth))
20
+ current_user_role && (meth.nil? || self.class.actions_get.include?(meth))
21
21
  end
22
22
  def authorize_rest_put(meth); false; end
23
23
  def after_initialize
@@ -38,7 +38,7 @@ module Marley
38
38
  end
39
39
  end
40
40
  class PrivateMessage < Message
41
- def get_actions;['reply','reply_all'];end
41
+ @actions_get=['reply','reply_all']
42
42
  def rest_cols; super << :recipients; end
43
43
  def current_user_role
44
44
  super || (recipients.match(/\b#{$request[:user][:name]}\b/) && "recipient")
@@ -73,7 +73,7 @@ module Marley
73
73
  end
74
74
  end
75
75
  class Post < Message
76
- def get_actions;['reply'];end
76
+ @actions_get=['reply']
77
77
  def current_user_role
78
78
  super || 'reader'
79
79
  end
@@ -10,9 +10,9 @@ module Marley
10
10
  attr_reader :menus
11
11
  attr_accessor :old_password,:password, :confirm_password
12
12
  def self.requires_user?
13
- ! ($request[:verb]=='rest_post')
13
+ ! ($request[:verb]=='rest_post' || ($request[:verb]=='rest_get' && $request[:path][1]=='new'))
14
14
  end
15
- def self.section
15
+ def self.rest_section
16
16
  ReggaeSection.new( {:title => 'User Info', :name => self.to_s.sub(/.*::/,'').underscore, :navigation => []}) if $request[:user].class == self
17
17
  end
18
18
  def write_cols;[:name,:email,:password,:confirm_password,:old_password];end
@@ -39,7 +39,7 @@ module Marley
39
39
  module Message
40
40
  def rest_associations
41
41
  if ! new?
42
- [ respond_to?(:public_tags) ? :public_tags : nil, respond_to?(:user_tags) ? user_tags_dataset.current_user_tags : nil].compact
42
+ [ respond_to?(:public_tags) ? :public_tags : nil, respond_to?(:user_tags) ? user_tags_dataset.current_user_dataset : nil].compact
43
43
  end
44
44
  end
45
45
  def new_tags
@@ -78,18 +78,18 @@ module Marley
78
78
  end
79
79
  class PrivateMessage < MJ::BasicMessaging::Resources::PrivateMessage
80
80
  attr_accessor :tags
81
+ @actions_get= superclass.actions_get << 'new_tags'
81
82
  @section_title='Private Messages'
82
83
  @section_name='pms'
83
84
  def self.section_navigation
84
85
  $request[:user].user_tags.map{|t| [:link,{:url => "/private_message?private_message[tag]=#{t.tag}",:title => t.tag.humanize}]}.unshift(PrivateMessage.reggae_link('new'))
85
86
  end
86
- def get_actions; super << 'new_tags';end
87
87
  def rest_schema
88
88
  super << [:text, :tags, 0,tags]
89
89
  end
90
90
  def reply
91
91
  r=super
92
- r.tags=(user_tags_dataset.current_user_tags.map{|t|t.tag} - RESERVED_PM_TAGS).join(',')
92
+ r.tags=(user_tags_dataset.current_user_dataset.map{|t|t.tag} - RESERVED_PM_TAGS).join(',')
93
93
  r
94
94
  end
95
95
  def after_create
@@ -104,7 +104,7 @@ module Marley
104
104
  def self.section_navigation
105
105
  MR::Tag.filter(:user_id => nil).map{|t| [:link,{:url => "/post?post[tag]=#{t.tag}",:title => t.tag.humanize}]}.unshift([:link,{:url => '/post?post[untagged]=true',:title => 'Untagged Messages'}]).unshift(Post.reggae_link('new'))
106
106
  end
107
- def get_actions;(super << 'new_user_tags') << 'new_tags';end
107
+ @actions_get=(superclass.actions_get << 'new_user_tags') << 'new_tags'
108
108
  def rest_schema
109
109
  (super << [:text, :tags, 0,tags] ) << [:text, :my_tags, 0,my_tags]
110
110
  end
@@ -5,7 +5,7 @@ module Marley
5
5
  class Tag < Sequel::Model
6
6
  def self.tagging_for(klass, user_class=nil,join_table=nil)
7
7
  current_user_tags=Module.new do
8
- def current_user_tags
8
+ def current_user_dataset
9
9
  filter(:tags__user_id => $request[:user][:id])
10
10
  end
11
11
  end
@@ -13,22 +13,16 @@ module Marley
13
13
  join_table||=:"#{tagged_class.table_name}_tags"
14
14
  klass_key=:"#{tagged_class.table_name.to_s.singularize}_id"
15
15
  tag_key=:tag_id
16
- attr_accessor klass_key
17
16
  if user_class
18
17
  UserTag.many_to_many klass.underscore.to_sym,:class => "Marley::Resources::#{klass}", :join_table => join_table,:left_key => tag_key,:right_key => klass_key,:extend => current_user_tags
19
- tagged_class.many_to_many :user_tags, :class => 'Marley::Resources::UserTag',:join_table => join_table,:left_key => klass_key,:right_key => tag_key, :extend => current_user_tags
18
+ tagged_class.many_to_many :user_tags, :class => 'Marley::Resources::UserTag',:join_table => join_table,:left_key => klass_key,:right_key => tag_key, :extend => [current_user_tags,Marley::RestActions]
20
19
  Marley::Resources.const_get(user_class).one_to_many :user_tags, :class => 'Marley::Resources::UserTag'
21
20
  UserTag.many_to_one user_class.underscore.to_sym,:class => "Marley::Resources::#{user_class}"
22
21
  else
23
22
  PublicTag.many_to_many klass.underscore.to_sym,:class => "Marley::Resources::#{klass}", :join_table => join_table,:left_key => tag_key,:right_key => klass_key
24
- tagged_class.many_to_many :public_tags,:class => "Marley::Resources::PublicTag",:join_table => join_table,:left_key => klass_key,:right_key => tag_key
23
+ tagged_class.many_to_many :public_tags,:class => "Marley::Resources::PublicTag",:join_table => join_table,:left_key => klass_key,:right_key => tag_key, :extend => Marley::RestActions
25
24
  end
26
25
  end
27
- def to_a
28
- a=super
29
- a[1][:delete_action]='remove_parent'
30
- a
31
- end
32
26
  def validate
33
27
  validates_presence :tag
34
28
  validates_unique [:tag,:user_id]
@@ -50,9 +44,11 @@ module Marley
50
44
  end
51
45
  class PublicTag < Tag
52
46
  set_dataset DB[:tags].filter(:user_id => nil)
47
+ @actions_delete='remove_parent'
53
48
  end
54
49
  class UserTag < Tag
55
50
  set_dataset DB[:tags].filter(~{:user_id => nil})
51
+ @actions_delete='remove_parent'
56
52
  end
57
53
  end
58
54
  end
data/lib/marley.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/ruby
2
2
  require 'json/ext'
3
3
  require 'json/add/core'
4
+ require 'marley/reggae'
5
+ require 'marley/utils'
4
6
  require 'rack'
5
7
  require 'rack/auth/basic'
6
8
  require 'rack/builder'
9
+ require 'sequel'
7
10
  require 'sequel/plugins/rest_convenience'
8
11
  require 'sequel/plugins/rest_auth'
9
12
  require 'marley/controllers'
10
- require 'marley/reggae'
11
13
  require 'logger'
12
14
  Sequel.extension :inflector
13
15
 
@@ -20,34 +22,17 @@ Sequel::Model.plugin :timestamps, :create => :date_created, :update => :date_upd
20
22
  log_fn='log/marley.log'
21
23
  $log=Logger.new(File.exists?(log_fn) ? log_fn : $stdout)
22
24
 
23
- # This is the main Marley namespace
24
25
  module Marley
25
26
  JOINT_DIRS=[File.expand_path("joints/",File.dirname(__FILE__)),"#{Dir.pwd}/joints"]
26
- # @see config
27
27
  DEFAULT_OPTS={:http_auth => true,:app_name => 'Application',:port => 1620,:default_user_class => :User, :auth_class => :User,:default_resource => 'Menu', :server => 'thin'}
28
28
  RESP_CODES={'get' => 200,'post' => 201,'put' => 204,'delete' => 204}
29
29
 
30
- # All constants in the Resources namespace should refer to public resources accessible to clients, subject, of course, to authentication/authorization.
31
- #
32
- # Resources MUST respond to one or more of #controller, #rest_get, #rest_post, #rest_put, #rest_delete
33
- #
34
- # If a Resource implements a #controller method, the method MUST return an object which responds to one or more of #rest_get, #rest_post, #rest_put, #rest_delete
35
30
  module Resources
36
31
  end
37
- # The default namespace for Joints. Joints MUST implement a #smoke method
38
32
  module Joints
39
33
  end
40
34
  require 'marley/joint' #this needs to happen after Marley::Resources is defined
41
35
 
42
- # Override default Marley configuration
43
- # @param [Hash] opts A hash containing Marley configuration options
44
- # @option opts [Boolean] :http_auth Whether or not to use http authentication
45
- # @option opts [String] :app_name Currently used by Jamaica to set page title
46
- # @option opts [Number] :port Port on which to run the server
47
- # @option opts [Object] :default_user_class The default class of the new user assigned to $request[:user] if no actual user is authenticated
48
- # @option opts [Object] :auth_class The class used to authenticate requests. MUST respond to #authenticate
49
- # @option opts [Object] :default_resource The resource called in response to a request to '/'
50
- # @option opts [String] :server the Rack web server to be used.
51
36
  def self.config(opts=nil)
52
37
  @marley_opts||=DEFAULT_OPTS
53
38
  @marley_opts.merge!(opts) if opts
@@ -55,17 +40,15 @@ module Marley
55
40
  @marley_opts
56
41
  end
57
42
 
58
- # Loads a joint and and calls its #smoke method
59
- # @param [String] joint_name The name of the joint to load and smoke - additional paramters passed through to the joint itself
60
43
  def self.joint(joint_name, *opts)
61
- joint_d=JOINT_DIRS.find {|d| File.exists?("#{d}/#{joint_name}.rb") }
62
- require "#{joint_d}/#{joint_name}"
63
- @marley_opts && @marley_opts[:client] && @marley_opts[:client].joint(joint_d,joint_name)
64
- joint=Marley::Joints.const_get(joint_name.camelize).new(*opts).smoke
44
+ unless Marley::Joints.constants.include?(joint_name.camelize)
45
+ joint_d=JOINT_DIRS.find {|d| File.exists?("#{d}/#{joint_name}.rb") }
46
+ require "#{joint_d}/#{joint_name}"
47
+ @marley_opts && @marley_opts[:client] && @marley_opts[:client].joint(joint_d,joint_name)
48
+ end
49
+ Marley::Joints.const_get(joint_name.camelize).new(*opts).smoke
65
50
  end
66
51
 
67
- # Runs the server
68
- # @param (see #config)
69
52
  def self.run(opts={})
70
53
  @marley_opts||=DEFAULT_OPTS
71
54
  marley_opts=@marley_opts.merge!(opts)
@@ -75,7 +58,7 @@ module Marley
75
58
  run(Marley::Router.new(marley_opts))
76
59
  }.to_app,{:Port => @marley_opts[:port]})
77
60
  end
78
- 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
61
+ class Router
79
62
  def initialize(opts={},app=nil)
80
63
  @opts=DEFAULT_OPTS.merge(opts)
81
64
  end
@@ -104,16 +87,23 @@ module Marley
104
87
  raise RoutingError unless Resources.constants.include?(rn)
105
88
  @resource=Resources.const_get(rn)
106
89
  raise AuthenticationError if @opts[:http_auth] && @resource.respond_to?('requires_user?') && @resource.requires_user? && $request[:user].new?
107
- @controller=@resource.respond_to?($request[:verb]) ? @resource : @resource.controller
90
+ @controller=nil
91
+ @controller=@resource.controller if @resource.respond_to?(:controller)
92
+ @controller=@resource if @resource.respond_to?($request[:verb])
93
+ raise RoutingError unless @controller
108
94
  json=@controller.send($request[:verb]).to_json
109
95
  html=@opts[:client] ? @opts[:client].to_s(json) : json
110
96
  resp_code=RESP_CODES[verb]
111
97
  headers||={'Content-Type' => "#{$request[:content_type]}; charset=utf-8"}
112
98
  [resp_code,headers,$request[:content_type].match(/json/) ? json : html]
113
99
  rescue Sequel::ValidationFailed
114
- ValidationError.new($!.errors).response
100
+ ValidationError.new($!.errors).to_a
115
101
  rescue
116
- ($!.class.new.respond_to?(:response) ? $!.class : MarleyError).new.response
102
+ if $!.class.superclass==MarleyError
103
+ $!.to_a
104
+ else
105
+ p $!,$!.backtrace
106
+ end
117
107
  ensure
118
108
  $log.info $request.merge({:request => nil,:user => $request[:user] ? $request[:user].name : nil})
119
109
  end
@@ -129,7 +119,7 @@ module Marley
129
119
  def log_error
130
120
  $log.fatal("#$!.message}\n#{$!.backtrace}")
131
121
  end
132
- def response
122
+ def to_a
133
123
  log_error
134
124
  json=[:error,{:error_type => self.class.name.underscore.sub(/_error$/,'').sub(/^marley\//,''),:description => self.class.description, :error_details => self.class.details}].to_json
135
125
  self.class.headers||={'Content-Type' => "#{$request[:content_type]}; charset=utf-8"}
@@ -166,11 +156,6 @@ module Marley
166
156
  $log.fatal("path:#{$request[:path]}\n msg:#{$!.message}\n backtrace:#{$!.backtrace}")
167
157
  end
168
158
  end
169
- module Utils
170
- def self.hash_keys_to_syms(hsh)
171
- hsh.inject({}) {|h,(k,v)| h[k.to_sym]= v.class==Hash ? hash_keys_to_syms(v) : v;h }
172
- end
173
- end
174
159
  end
175
160
  MR=Marley::Resources
176
161
  MJ=Marley::Joints
data/lib/marley/joint.rb CHANGED
@@ -5,6 +5,9 @@ module Marley
5
5
  # - ClassMethods - Modules within this module will extend any constant in Marley::Resources with the same name.
6
6
  # - InstanceMethods - Modules within this module will append their features to any constant in Marley::Resources with the same name.
7
7
  class Joint
8
+ #def self.provides(*args)
9
+ # [:resources,:class_methods,:]
10
+ #end
8
11
  def initialize(opts={})
9
12
  config(opts)
10
13
  end
@@ -20,6 +23,7 @@ module Marley
20
23
  end
21
24
  end
22
25
  end
26
+ self
23
27
  end
24
28
  def config(opts)
25
29
  @opts=(@opts || {}).merge(opts)
data/lib/marley/reggae.rb CHANGED
@@ -1,4 +1,12 @@
1
1
 
2
+ RESTRICT_HIDE=1
3
+ RESTRICT_RO=2
4
+ RESTRICT_REQ=4
5
+ TYPE_INDEX=0
6
+ NAME_INDEX=1
7
+ RESTRICTIONS_INDEX=2
8
+ require 'sequel'
9
+ Sequel.extension :inflector
2
10
  module Marley
3
11
  # @see file:reggae.ebnf for Raggae sytax
4
12
  class Reggae < Array
@@ -21,7 +29,7 @@ module Marley
21
29
  attr_accessor :properties,:contents
22
30
  # @param [Array] *args an array in Reggae syntax
23
31
  def initialize(*args)
24
- super
32
+ super
25
33
  if is_resource?
26
34
  @resource_type=self[0]=self[0].to_sym
27
35
  self[1]=Utils.hash_keys_to_syms(self[1]) if self[1].class==Hash
@@ -29,11 +37,11 @@ module Marley
29
37
  @contents=self[2 .. -1]
30
38
  self.class.mk_prop_methods
31
39
  else
32
- replace(map {|r| Reggae.new(r).to_resource})
40
+ replace(map {|r| r.class==Array ? Reggae.new(r).to_resource : r})
33
41
  end
34
42
  end
35
43
  def is_resource?
36
- [String, Symbol].include?(self[0].class) && self[1].class==Hash
44
+ [String, Symbol].include?(self[0].class) && Marley.constants.include?("reggae_#{self[0]}".camelcase)
37
45
  end
38
46
  def contents=(*args)
39
47
  self[2]=*args
@@ -55,7 +63,7 @@ module Marley
55
63
  def initialize(*args)
56
64
  @resource_type=self.class.to_s.sub(/.*Reggae/,'').underscore.to_sym
57
65
  if args[0].class==Hash
58
- initialize [@resource_type,args[0]]
66
+ initialize [@resource_type,args[0],args[1 .. -1]]
59
67
  else
60
68
  super
61
69
  end
@@ -71,7 +79,7 @@ module Marley
71
79
  properties :title,:description,:url
72
80
  end
73
81
  class ReggaeInstance < ReggaeResource
74
- properties :name,:new_rec,:schema,:search,:url,:get_actions,:delete_action
82
+ properties :name,:new_rec,:schema,:search,:url,:actions
75
83
  attr_accessor :schema
76
84
  def initialize(*args)
77
85
  super
@@ -84,9 +92,6 @@ module Marley
84
92
  params
85
93
  end
86
94
  end
87
- def instance_action_url(action_name)
88
- "#{url}#{action_name}" if get_actions.include?(action_name.to_s)
89
- end
90
95
  def col_value(col_name,col_value=nil)
91
96
  col=@schema[col_name]
92
97
  col.col_value=col_value if col_value
@@ -98,7 +103,7 @@ module Marley
98
103
  end
99
104
  end
100
105
  class ReggaeInstanceList < ReggaeResource
101
- properties :name,:description,:get_actions,:delete_action,:items
106
+ properties :name,:description,:actions,:items
102
107
  #not implemented yet
103
108
  end
104
109
  class ReggaeMsg < ReggaeResource
@@ -109,7 +114,7 @@ module Marley
109
114
  end
110
115
  class ReggaeSchema < Array
111
116
  def initialize(*args)
112
- super
117
+ super
113
118
  replace(map{|spec| ReggaeColSpec.new(spec)})
114
119
  end
115
120
  def [](i)