marley 0.3.0 → 0.4.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/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)