marley 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,48 +1,75 @@
1
1
  =Marley
2
2
 
3
- Marley is a project consisting of several parts:
3
+ Marley is a framework for quickly building RESTful web services and applications. It consists of several parts:
4
4
 
5
- * A server side micro-framework for returning data from ORM models (currently only Sequel) and other objects (Ruby, based on rack with thin as the default web server)
6
- * A framework for adding reusable content to the framework ("Joints")
7
- * A JSON data format ("Reggae")
8
- * A simple JS client ("Jamaica") which renders Reggae as HTML.
5
+ * A simple Rack application that acts as request parser/router.
6
+ * A default controller for ORM model classes.
7
+ * Two plugins for the Sequel ORM.
8
+ * RestConvenience - Connects the class to a default controller.
9
+ * RestAuthorization - Adds default authorization methods to a model class and its instances.
10
+ * Marley Joints - A framework for creating reusable Marley resource sets.
11
+ * Reggae - A JSON data format
12
+ * Jamaica - A minimal JS/jQuery client which renders Reggae resources as browser DOM objects.
9
13
 
10
- The point of the whole thing is to create as many useful default behaviors as possible, while making it easy to override any or all of them.
14
+ The point of the whole thing is to minimize the amount of non-model (non-datacentric) code that needs to be written to implement a web service/application.
11
15
 
12
- Please see the examples/forum.rb to get a basic idea of what a Marley application looks like.
16
+ Please see the examples/forum.rb and the included Joints' code to get a basic idea of what a Marley application looks like.
13
17
 
14
18
  -----
15
19
 
16
- More about each part:
20
+ ==Marley Resources
17
21
 
18
- ==Marley
22
+ Marley resources are constants in the Marley::Resources namespace.
19
23
 
20
- * Marley resources are constants in the Marley::Resources namespace.
21
- * A resource must respond either to a #controller method or to one or more of #rest_get, #rest_post, #rest_put, or #rest_delete.
22
- * Resources implementing a #controller method should return an object that responds to one or more REST verbs from that method
23
- * REST verb methods should return an object which responds to #to_json by returning a string to be sent to the client
24
+ * A resource must respond either to a #controller method or to one or more REST verbs (#rest_get, #rest_post, #rest_put, or #rest_delete).
25
+ * A #controller method must return an object that responds to one or more REST verbs.
26
+ * REST verb methods should return an object with a #to_json method.
24
27
 
25
- * Marley provides 2 plugins for the Sequel ORM.
26
- * RestConvenience - Adds a default controller for standard rest routes to a model
27
- * RestAuthorization - Adds default authorization to a model
28
28
 
29
- I use Sequel exclusively, so I've only written these plugins for it. I imagine it would be pretty trivial to port them to other ORMs.
29
+ ==The Parser/Router
30
+
31
+ The parser splits the request path on '/' and treats the first part as the requested resource. If no resource is specified in the request, it uses the default resource.
32
+
33
+ if the resource has a REST method corresponding the the request verb, the parser calls that method and sends the return value to the client.
34
+
35
+ The parser also traps various errors and returns the appropriate error message to the client.
36
+
37
+ == The Sequel plugins
38
+
39
+ Marley activates both of them for Sequel::Model. This will soon change to an option.
40
+
41
+ ==The Default Model Controller
42
+
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
+
30
45
 
31
46
  ==Joints
32
47
 
33
- "Joints" are pre-packaged resource sets that can be included in a Marley application. The joints API is very much a work in progress.
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.
34
59
 
35
- For now, there are 3 joints included in the Marley distribution:
60
+ For now, there are 5 joints included in the Marley distribution:
36
61
 
37
62
  * Basic User
38
63
  * Basic Messaging
64
+ * Basic Menu System
39
65
  * Tagging
66
+ * Tagged Messaging
40
67
 
41
- With a bit of configuration and a few menus, these comprise the example forum application, which is in turn the targt for the test suite.
68
+ With a bit of configuration, these comprise the example forum application, which is in turn the targt for the test suite.
42
69
 
43
70
  ==Reggae
44
71
 
45
- The server and client use a JSON based data representation I developed for this project and tentatively named "Reggae." It is documented roughly in Reggae.ebnf. I am considering some structural changes, which are reflected in Reggae2.ebnf. I'd love some comments on this.
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.
46
73
 
47
74
  ==Jamaica
48
75
 
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ task :default => [:test]
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.ruby_opts=['-r test/test_include']
9
+ t.test_files=FileList['test/*_tests.rb']
10
+ end
11
+
data/examples/blog.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'sequel'
3
+ require 'digest/sha1'
4
+
5
+ $: << "#{File.dirname(__FILE__)}/../lib/"
6
+
7
+ APP_DIR=File.dirname(__FILE__)
8
+ require "marley"
9
+ require "client/jamaica"
10
+ #need to automate the following somehow but can't think of anything that isn't ugly ATM
11
+ DB=Sequel.sqlite("#{APP_DIR}/forum#{ENV["MARLEY_TESTING"] ? '_test' : ''}.sqlite3")#,:loggers => [Logger.new($stdout)])
12
+
13
+ Marley.config({:app_name => 'The Forum',:client => Marley::Client.new({:app_name => 'The Forum'})})
14
+
15
+ Marley.joint 'tagged_messaging', {:resources => ['user','admin']}
16
+ Marley.joint 'basic_menu_system'
17
+
18
+ module Marley
19
+ module Resources
20
+ class Author < User
21
+ def self.requires_user?;true;end
22
+ end
23
+ class Post < MJ::TaggedMessaging::Resources::Post
24
+ end
25
+ end
26
+ end
Binary file
Binary file
@@ -6,7 +6,7 @@ EXAMPLES_DIR=File.dirname(__FILE__) + '/../examples'
6
6
  ENV['MARLEY_TESTING']='true'
7
7
  `cp #{EXAMPLES_DIR}/empty.sqlite3 #{EXAMPLES_DIR}/forum_test.sqlite3`
8
8
  require "#{EXAMPLES_DIR}/forum.rb"
9
- require "#{EXAMPLES_DIR}/../lib/test_helpers"
9
+ require "#{EXAMPLES_DIR}/../lib/marley/test_helpers"
10
10
 
11
11
  class UserTests < Test::Unit::TestCase
12
12
  def setup
@@ -24,18 +24,18 @@ class UserTests < Test::Unit::TestCase
24
24
  end
25
25
  should "validate new user properly" do
26
26
  assert resp=@client.create
27
- assert_equal "error", resp.resource_type
27
+ assert_equal :error, resp.resource_type
28
28
  assert_equal "validation", resp.error_type
29
29
  assert_equal ["is required"], resp.error_details[:name]
30
30
  resp=@client.create({:'user[name]' => 'asdf'})
31
- assert_equal "error", resp.resource_type
31
+ assert_equal :error, resp.resource_type
32
32
  assert_equal "validation", resp.error_type
33
33
  resp=@client.create({:'user[name]' => 'asdf',:'user[password]' => 'asdfaf'})
34
- assert_equal "error", resp.resource_type
34
+ assert_equal :error, resp.resource_type
35
35
  assert_equal "validation", resp.error_type
36
36
  assert_equal ["Password must contain at least 8 characters"], resp.error_details[:password]
37
37
  resp=@client.create(:'user[name]' => 'asdf',:'user[password]' => 'asdfasdf')
38
- assert_equal "error", resp.resource_type
38
+ assert_equal :error, resp.resource_type
39
39
  assert_equal "validation", resp.error_type
40
40
  assert_equal ["Passwords do not match"], resp.error_details[:confirm_password]
41
41
  end
@@ -64,7 +64,7 @@ class UserTests < Test::Unit::TestCase
64
64
  params=user.to_params
65
65
  assert @client.update(params,{:code => 204})
66
66
  assert err=@client.update(params.update('user[password]' => 'zxcvzxcv'),{:code => 400})
67
- assert_equal "error", err.resource_type
67
+ assert_equal :error, err.resource_type
68
68
  assert_equal "validation", err.error_type
69
69
  assert @client.update(params.update('user[password]' => 'zxcvzxcv','user[confirm_password]' => 'zxcvzxcv', 'user[old_password]' => 'asdfasdf'),:code => 204)
70
70
  assert @client.read({},:code => 401)
@@ -104,18 +104,18 @@ class MessageTests < Test::Unit::TestCase
104
104
  should "validate new user generated PMs properly" do
105
105
  #reject a PM with only recipients
106
106
  resp=@client.create({:'private_message[recipients]' => 'user2'},{:code => 400})
107
- assert_equal "error", resp.resource_type
107
+ assert_equal :error, resp.resource_type
108
108
  assert_equal "validation", resp.error_type
109
109
  assert_equal ["is required"], resp.error_details[:title]
110
110
  assert_equal ["is required"], resp.error_details[:message]
111
111
  #reject a PM to a non-existent user
112
112
  resp=@client.create({:'private_message[recipients]' => 'asdfasdfasdfasdf',:'private_message[title]' => 'asdf',:'private_message[message]' => 'asdf'},{:code => 400})
113
- assert_equal "error", resp.resource_type
113
+ assert_equal :error, resp.resource_type
114
114
  assert_equal "validation", resp.error_type
115
115
  assert resp.error_details[:recipients][0]
116
116
  #reject a PM from user to user
117
117
  resp=@client.create({:'private_message[recipients]' => 'user2',:'private_message[title]' => 'asdf',:'private_message[message]' => 'asdf'},{:code => 400})
118
- assert_equal "error", resp.resource_type
118
+ assert_equal :error, resp.resource_type
119
119
  assert_equal "validation", resp.error_type
120
120
  assert resp.error_details[:recipients][0]
121
121
  end
@@ -129,7 +129,7 @@ class MessageTests < Test::Unit::TestCase
129
129
  end
130
130
  should "validate new admin generated PMs properly" do
131
131
  resp=@client.create({:'private_message[recipients]' => 'user2'},{:code => 400})
132
- assert_equal "error", resp.resource_type
132
+ assert_equal :error, resp.resource_type
133
133
  assert_equal "validation", resp.error_type
134
134
  assert_equal ["is required"], resp.error_details[:title]
135
135
  assert_equal ["is required"], resp.error_details[:message]
@@ -298,7 +298,7 @@ class MessageTests < Test::Unit::TestCase
298
298
  context 'validation' do
299
299
  should "get a validation error trying to post without a title or message as admin, user1, or user2" do
300
300
  resp=@client.create({},{:code => 400,:auth => @admin_auth})
301
- assert_equal "error", resp.resource_type
301
+ assert_equal :error, resp.resource_type
302
302
  assert_equal "validation", resp.error_type
303
303
  assert_equal ["is required"], resp.error_details[:title]
304
304
  assert_equal ["is required"], resp.error_details[:message]
@@ -5,23 +5,30 @@ module Sequel::Plugins::RestSection
5
5
  SECTION_PROPS.each {|p| attr_accessor :"section_#{p}"}
6
6
  def section
7
7
  if SECTION_PROPS.find {|p| send(:"section_#{p}").to_s > ''}
8
- Marley::ReggaeSection.new [SECTION_PROPS.inject({}) {|props,p| props[p.to_sym]=send(:"section_#{p}");props }]
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)
9
13
  end
10
14
  end
11
15
  end
12
16
  end
13
- Sequel::Model.plugin :rest_section
14
17
  module Marley
15
18
  module Joints
16
19
  class BasicMenuSystem < Joint
20
+ def smoke
21
+ super
22
+ Sequel::Model.plugin :rest_section
23
+ end
17
24
  module Resources
18
25
  class Menu
19
26
  class <<self
20
27
  attr_accessor :sections
21
28
  end
22
- attr_accessor :title,:name,:description, :navigation
29
+ include Sequel::Plugins::RestSection::ClassMethods
23
30
  def self.rest_get
24
- new.to_json
31
+ new.section
25
32
  end
26
33
  def self.requires_user?
27
34
  ! $request[:path].to_a.empty?
@@ -31,22 +38,19 @@ module Marley
31
38
  if $request[:user].new?
32
39
  u=$request[:user].to_a
33
40
  u[1].merge!({:description => 'If you don\'t already have an account, please create one here:'})
34
- @title="Welcome to #{$request[:opts][:app_name]}"
35
- @description='Login or signup here.'
36
- @navigation=[LOGIN_FORM,u]
41
+ @section_title="Welcome to #{$request[:opts][:app_name]}"
42
+ @section_description='Login or signup here.'
43
+ @section_navigation=[LOGIN_FORM,u]
37
44
  else
38
- @title = "#{$request[:opts][:app_name]} Main Menu"
39
- @description="Welcome to #{$request[:opts][:app_name]}, #{$request[:user].name}"
40
- @navigation=(self.class.sections || MR.constants).map do |rn|
41
- if (resource=MR.const_get(rn)).respond_to?(:section) && (s=resource.section)
45
+ @section_title = "#{$request[:opts][:app_name]} Main Menu"
46
+ @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
42
49
  [:link,{:title => s.title, :description =>s.description, :url => "#{resource.resource_name}/section" }]
43
50
  end
44
51
  end.compact
45
52
  end
46
53
  end
47
- def to_json
48
- [:section,{:title => @title,:description => @description,:name => @name, :navigation => @navigation}]
49
- end
50
54
  end
51
55
  end
52
56
  end
@@ -6,6 +6,7 @@ module Marley
6
6
  module Resources
7
7
  class Message < Sequel::Model
8
8
  plugin :single_table_inheritance, :message_type, :model_map => lambda{|v| v ? MR.const_get(v.to_s) : ''}, :key_map => lambda{|klass|klass.name.sub(/.*::/,'')}
9
+ # CHANGE NEEDED:tree is unnecessary, instead, select by thread_id, order by parent_id, inject [] for nesting. Should be much faster.
9
10
  plugin :tree
10
11
  many_to_one :author, :class => :'Marley::Resources::User'
11
12
  @owner_col=:author_id
@@ -22,7 +23,6 @@ module Marley
22
23
  def after_initialize
23
24
  super
24
25
  if new?
25
- self.author_id=$request[:user][:id]
26
26
  self.thread_id=parent ? parent.thread_id : Message.select(:max.sql_function(:thread_id).as(:tid)).all[0][:tid].to_i + 1
27
27
  end
28
28
  end
@@ -13,7 +13,7 @@ module Marley
13
13
  ! ($request[:verb]=='rest_post')
14
14
  end
15
15
  def self.section
16
- ReggaeSection.new([ {:title => 'User Info', :name => self.to_s.underscore, :navigation => []},$request[:user]]) if $request[:user].class == self
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
19
19
  def rest_schema
@@ -9,9 +9,11 @@ module Marley
9
9
  class TaggedMessaging < Joint
10
10
  def smoke
11
11
  super
12
- MR::Tag.tagging_for('PrivateMessage', 'User')
13
- MR::Tag.tagging_for('Post', 'User')
14
- MR::Tag.tagging_for('Post')
12
+ MR::Tag.tagging_for('PrivateMessage', 'User') if MR.constants.include?('PrivateMessage')
13
+ if MR.constants.include?('Post')
14
+ MR::Tag.tagging_for('Post', 'User')
15
+ MR::Tag.tagging_for('Post')
16
+ end
15
17
  end
16
18
  module ClassMethods
17
19
  module Message
File without changes
@@ -1,7 +1,5 @@
1
1
  module Marley
2
2
  module Joints
3
- MR=Marley::Resources
4
- MJ=Marley::Joints
5
3
  class Joint
6
4
  def initialize(opts={})
7
5
  config(opts)
@@ -15,17 +15,18 @@ module Marley
15
15
  end
16
16
  def initialize(*args)
17
17
  super
18
+ self[1]=Utils.hash_keys_to_syms(self[1]) if self[1].class==Hash
18
19
  self.class.mk_prop_methods
19
20
  end
21
+ def properties
22
+ self[1] || nil
23
+ end
20
24
  def resource_type
21
- [String, Symbol].include?(self[0].class) ? self[0].to_s : nil
25
+ [String, Symbol].include?(self[0].class) ? self[0].to_sym : nil
22
26
  end
23
27
  def is_resource?
24
28
  ! resource_type.nil?
25
29
  end
26
- def properties
27
- self[1].class==Hash ? Utils.hash_keys_to_syms(self[1]) : nil
28
- end
29
30
  def contents
30
31
  is_resource? ? Reggae.new(self[2 .. -1]) : nil
31
32
  end
@@ -37,7 +38,7 @@ module Marley
37
38
  super.class==Array ? Reggae.new(super).to_resource : super
38
39
  end
39
40
  def to_resource
40
- is_resource? ? Marley.const_get("Reggae#{resource_type.camelize}".to_sym).new(self) : self
41
+ is_resource? ? Marley.const_get("Reggae#{resource_type.to_s.camelize}".to_sym).new(self) : self
41
42
  end
42
43
  def find_instances(rn,instances=Reggae.new([]))
43
44
  if self.class==ReggaeInstance && self.name.to_s==rn
@@ -49,9 +50,15 @@ module Marley
49
50
  end
50
51
  end
51
52
  class ReggaeResource < Reggae
53
+ def resource_type
54
+ self.class.to_s.sub(/.*Reggae/,'').underscore.to_sym
55
+ end
52
56
  def initialize(*args)
53
- super
54
- unshift self.class.to_s.sub(/.*Reggae/,'').underscore.to_sym unless is_resource?
57
+ if args[0].class==Hash
58
+ initialize [resource_type,args[0]]
59
+ else
60
+ super
61
+ end
55
62
  end
56
63
  end
57
64
  class ReggaeSection < ReggaeResource
@@ -64,27 +71,33 @@ module Marley
64
71
  self.valid_properties=[:title,:description,:url]
65
72
  end
66
73
  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])
74
+ self.valid_properties=[:name,:new_rec,:schema,:search,:url,:get_actions,:delete_action]
75
+ def initialize(*args)
76
+ super
77
+ self.schema=ReggaeSchema.new(self.schema)
70
78
  end
71
79
  def to_params
72
- resource_name=name
73
80
  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)
81
+ params["#{name}[#{spec.col_name}]"]=spec.col_value unless (spec.col_restrictions & RESTRICT_RO > 0)
76
82
  params
77
83
  end
78
84
  end
79
85
  def instance_action_url(action_name)
80
86
  "#{url}#{action_name}" if get_actions.include?(action_name.to_s)
81
87
  end
88
+ def col_value(col_name,col_value=nil)
89
+ col=schema[col_name]
90
+ col.col_value=col_value if col_value
91
+ col.col_value
92
+ end
93
+ def set_values(col_hash)
94
+ col_hash.each_pair {|k,v| col_value(k,v)}
95
+ self
96
+ end
82
97
  end
83
98
  class ReggaeInstanceList < ReggaeResource
84
99
  self.valid_properties=[:name,:description,:get_actions,:delete_action,:items]
85
- def schema
86
- ReggaeSchema.new(self.properties[:schema])
87
- end
100
+ #not implemented yet
88
101
  end
89
102
  class ReggaeMsg < ReggaeResource
90
103
  self.valid_properties=[:title,:description]
@@ -93,11 +106,15 @@ module Marley
93
106
  self.valid_properties=[:error_type,:description,:error_details]
94
107
  end
95
108
  class ReggaeSchema < Array
109
+ def initialize(*args)
110
+ super
111
+ replace(map{|spec| ReggaeColSpec.new(spec)})
112
+ end
96
113
  def [](i)
97
114
  if i.class==Fixnum
98
- ReggaeColSpec.new(super)
115
+ super
99
116
  else
100
- self[find_index {|cs|ReggaeColSpec.new(cs).col_name==i.to_s}]
117
+ find {|cs|cs.col_name.to_s==i.to_s}
101
118
  end
102
119
  end
103
120
  end
@@ -1,5 +1,5 @@
1
1
  require "rack/test"
2
- require 'reggae'
2
+ require 'marley/reggae'
3
3
  module Marley
4
4
  #simple mocking framework; could be expanded to a general use client by adding display code.
5
5
  class TestClient
@@ -18,9 +18,11 @@ module Marley
18
18
  opts[:url] || '/' + [:root_url, :resource_name, :instance_id, :method].map {|k| opts[k]}.compact.join('/') + opts[:extention].to_s
19
19
  end
20
20
  def process(verb,params={},opts={})
21
+ #p 'params:',params,'opts:',opts,"-------------" if opts
21
22
  opts||={}
22
23
  opts=@opts.merge(opts)
23
24
  expected_code=opts[:code] || RESP_CODES[verb]
25
+ params=params.to_params if params.respond_to?(:to_params)
24
26
  if opts[:debug]
25
27
  p opts
26
28
  p "#{verb} to: '#{make_url(opts)}'"
@@ -30,14 +32,16 @@ module Marley
30
32
  authorize opts[:auth][0],opts[:auth][1] if opts[:auth]
31
33
  header 'Authorization',nil unless opts[:auth] #clear auth from previous requests
32
34
  send(verb,make_url(opts),params)
35
+ #p last_response.body if opts[:debug]
36
+ #p JSON.parse(last_response.body) if opts[:debug]
33
37
  p last_response.status if opts[:debug]
34
38
  p expected_code if opts[:debug]
35
39
  return false unless (expected_code || RESP_CODES[method])==last_response.status
36
40
  Reggae.get_resource(JSON.parse(last_response.body)) rescue last_response.body
37
41
  end
38
42
  ['create','read','update','del'].each do |op|
39
- define_method op.to_sym, Proc.new { |params,opts|
40
- process(CRUD2REST[op],params,opts)
43
+ define_method op.to_sym, Proc.new { |*args|
44
+ process(CRUD2REST[op],args[0],args[1])
41
45
  }
42
46
  end
43
47
  DEFAULT_OPTS.keys.each do |opt|
data/lib/marley.rb CHANGED
@@ -3,9 +3,10 @@ require 'json/ext'
3
3
  require 'rack'
4
4
  require 'rack/auth/basic'
5
5
  require 'rack/builder'
6
- require 'sequel_plugins'
7
- require 'controllers'
8
- require 'reggae'
6
+ require 'sequel/plugins/rest_convenience'
7
+ require 'sequel/plugins/rest_auth'
8
+ require 'marley/controllers'
9
+ require 'marley/reggae'
9
10
  require 'logger'
10
11
  Sequel.extension :inflector
11
12
 
@@ -26,7 +27,9 @@ module Marley #The main Marley namespace.
26
27
 
27
28
  module Resources #All objects in the Resources namespace are exposed by the server.
28
29
  end
29
- require 'joint' #this needs to happen after Marley::Resources is defined
30
+ module Joints
31
+ end
32
+ require 'marley/joint' #this needs to happen after Marley::Resources is defined
30
33
  def self.config(opts=nil)
31
34
  @marley_opts||=DEFAULT_OPTS
32
35
  @marley_opts.merge!(opts) if opts
@@ -36,7 +39,7 @@ module Marley #The main Marley namespace.
36
39
  def self.joint(joint_name, *opts)
37
40
  joint_d=JOINT_DIRS.find {|d| File.exists?("#{d}/#{joint_name}.rb") }
38
41
  require "#{joint_d}/#{joint_name}"
39
- @marley_opts[:client] && @marley_opts[:client].joint(joint_d,joint_name)
42
+ @marley_opts && @marley_opts[:client] && @marley_opts[:client].joint(joint_d,joint_name)
40
43
  joint=Marley::Joints.const_get(joint_name.camelize).new(*opts).smoke
41
44
  end
42
45
  def self.run(opts={})
@@ -145,4 +148,6 @@ module Marley #The main Marley namespace.
145
148
  end
146
149
  end
147
150
  end
151
+ MR=Marley::Resources
152
+ MJ=Marley::Joints
148
153
  at_exit {Marley.run if ARGV[0]=='run'}
@@ -0,0 +1,53 @@
1
+
2
+ module Sequel::Plugins::RestAuthorization
3
+ module ClassMethods
4
+ attr_accessor :owner_col, :allowed_get_methods
5
+ def inherited(c)
6
+ super
7
+ c.owner_col=@owner_col
8
+ c.allowed_get_methods=@allowed_get_methods
9
+ end
10
+ def requires_user?(verb=nil,meth=nil);true;end
11
+ def authorize(meth)
12
+ if respond_to?(auth_type="authorize_#{$request[:verb]}")
13
+ send(auth_type,meth)
14
+ else
15
+ case $request[:verb]
16
+ when 'rest_put','rest_delete'
17
+ false
18
+ when 'rest_post'
19
+ new($request[:post_params][resource_name.to_sym]||{}).current_user_role=='owner' && meth.nil?
20
+ when 'rest_get'
21
+ methods=@allowed_get_methods || ['section','list','new']
22
+ (methods.class==Hash ? methods[$request[:user].class] : methods).include?(meth)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ module InstanceMethods
28
+ def after_initialize
29
+ super
30
+ send("#{self.class.owner_col}=",$request[:user][:id]) if $request && self.class.owner_col && new?
31
+ end
32
+ def requires_user?(verb=nil,meth=nil);true;end
33
+ def authorize(meth)
34
+ if respond_to?(auth_type="authorize_#{$request[:verb]}")
35
+ send(auth_type,meth)
36
+ else
37
+ current_user_role=='owner'
38
+ end
39
+ end
40
+ def current_user_role
41
+ "owner" if owners.include?($request[:user])
42
+ end
43
+ def owners
44
+ if self.class.to_s.match(/User$/)||self.class.superclass.to_s.match(/User$/)
45
+ [self]
46
+ elsif @owner_col
47
+ [User[send(@owner_col)]]
48
+ else
49
+ 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
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,4 @@
1
+
1
2
  RESTRICT_HIDE=1
2
3
  RESTRICT_RO=2
3
4
  RESTRICT_REQ=4
@@ -20,7 +21,7 @@ module Sequel::Plugins::RestConvenience
20
21
  if user.respond_to?(otm=self.resource_name.pluralize)
21
22
  if user.method(otm).arity==0
22
23
  if (relationship=user.send(otm)).respond_to?(:filter)
23
- relationship.filter($request[:get_params][resource_name.to_sym])
24
+ relationship.filter($request[:get_params][resource_name.to_sym] || {}).all
24
25
  else
25
26
  user.send(otm)
26
27
  end
@@ -69,7 +70,7 @@ module Sequel::Plugins::RestConvenience
69
70
  respond_to?('name') ? name : id.to_s
70
71
  end
71
72
  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
+ a=Marley::ReggaeInstance.new( {:name => self.class.resource_name,:url => url ,:new_rec => self.new?,:schema => rest_schema,:get_actions => get_actions})
73
74
  if respond_to?(:rest_associations) && ! new?
74
75
  a.contents=rest_associations.map do |assoc|
75
76
  (assoc.class==Symbol ? send(assoc) : assoc).map{|instance| instance.to_a}
@@ -88,54 +89,3 @@ module Sequel::Plugins::RestConvenience
88
89
  end
89
90
  end
90
91
  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/marley-0.1.0.gem ADDED
Binary file
data/marley.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{marley}
5
- s.version = "0.1.0"
5
+ s.version = "0.2.0"
6
6
  s.summary = %q{Irie default restful routes for your models and other objects}
7
7
  s.description = %q{Marley implements a web services microframework on top of Rack and Sequel on the server side and Jquery on the client side.}
8
8
  s.authors = ["Herb Daily"]
Binary file
@@ -0,0 +1,9 @@
1
+
2
+
3
+ class MenuTests < Test::Unit::TestCase
4
+ def setup
5
+ end
6
+ should 'work' do
7
+ assert true
8
+ end
9
+ end
@@ -0,0 +1,289 @@
1
+
2
+ class MessageTests < Test::Unit::TestCase
3
+ def initialize(*args)
4
+ super
5
+ MR::User.delete
6
+ @client=Marley::TestClient.new(:resource_name => 'user')
7
+ ['user1','user2','admin'].each do |un|
8
+ MR::User.new(:name => un,:password => 'asdfasdf', :confirm_password => 'asdfasdf').save
9
+ end
10
+ MR::User[:name => 'admin'].update(:user_type => 'Admin')
11
+ @admin_auth=['admin','asdfasdf']
12
+ @user1_auth=['user1','asdfasdf']
13
+ @user2_auth=['user2','asdfasdf']
14
+ end
15
+ def setup
16
+ MR::Message.delete
17
+ MR::Tag.delete
18
+ DB[:messages_tags].delete
19
+ end
20
+ context "Private Messages" do
21
+ setup do
22
+ @client.resource_name='private_message'
23
+ @pm=MR::PrivateMessage.new.to_a
24
+ end
25
+ context "regular user (user1) logged in" do
26
+ setup do
27
+ @client.auth=@user1_auth
28
+ end
29
+ should "show PM list and new PM form" do
30
+ assert @client.read({})
31
+ assert @client.read({}, :method => 'new')
32
+ end
33
+ should "validate new user generated PMs properly" do
34
+ #reject a PM with only recipients
35
+ @pm.col_value(:recipients, 'user2')
36
+ resp=@client.create(@pm,{:code => 400})
37
+ assert_equal :error, resp.resource_type
38
+ assert_equal "validation", resp.error_type
39
+ assert_equal ["is required"], resp.error_details[:title]
40
+ assert_equal ["is required"], resp.error_details[:message]
41
+ #reject a PM from user to user
42
+ @pm.set_values({:title => 'asdf', :message => 'asdf'})
43
+ resp=@client.create(@pm,{:code => 400})
44
+ assert_equal :error, resp.resource_type
45
+ assert_equal "validation", resp.error_type
46
+ assert resp.error_details[:recipients][0]
47
+ #reject a PM to a non-existent user
48
+ @pm.col_value(:recipients, 'asdfasdfasdf')
49
+ resp=@client.create(@pm,{:code => 400})
50
+ assert_equal :error, resp.resource_type
51
+ assert_equal "validation", resp.error_type
52
+ assert resp.error_details[:recipients][0]
53
+ #accept a PM to admin
54
+ @pm.col_value(:recipients, 'admin')
55
+ assert @client.create({:'private_message[recipients]' => 'admin',:'private_message[title]' => 'asdf',:'private_message[message]' => 'asdf'})
56
+ end
57
+ end
58
+ context "admin logged in" do
59
+ setup do
60
+ @client.auth=@admin_auth
61
+ end
62
+ should "validate new admin generated PMs properly" do
63
+ resp=@client.create(@pm.set_values(:recipients => 'user2'),{:code => 400})
64
+ assert_equal :error, resp.resource_type
65
+ assert_equal "validation", resp.error_type
66
+ assert_equal ["is required"], resp.error_details[:title]
67
+ assert_equal ["is required"], resp.error_details[:message]
68
+ end
69
+ should "accept a PM to user1" do
70
+ assert @client.create(@pm.set_values(:'recipients' => 'user1',:'title' => 'asdf',:'message' => 'asdf'))
71
+ end
72
+ end
73
+ context "message with no tags" do
74
+ setup do
75
+ @client.auth=@admin_auth
76
+ @client.create(@pm.set_values({:'recipients' => 'user1',:'title' => 'asdf',:'message' => 'asdf'}))
77
+ end
78
+ should "show up in PM list of sender and receiver" do
79
+ resp=@client.read({})
80
+ assert_equal 1, resp.length
81
+ resp=@client.read({},{:auth => @user1_auth})
82
+ assert_equal 1, resp.length
83
+ end
84
+ should "have sent tag for sender" do
85
+ resp=@client.read({})
86
+ assert_equal 3, resp[0].length
87
+ assert_equal "sent", resp.find_instances('user_tag')[0].schema[:tag].col_value
88
+ end
89
+ should "have inbox tag for receiver" do
90
+ resp=@client.read({},{:auth => @user1_auth})
91
+ assert_equal 3, resp[0].length
92
+ assert_equal "inbox", resp.find_instances('user_tag')[0].schema[:tag].col_value
93
+ end
94
+ should "have reply, reply_all and new_tags instance get actions" do
95
+ resp=@client.read({})
96
+ assert_same_elements ['reply','reply_all','new_tags'], resp[0].get_actions
97
+ end
98
+ context "user1 instance actions" do
99
+ setup do
100
+ @client.auth=@user1_auth
101
+ @msg=@client.read({})[0]
102
+ @client.instance_id=@msg.schema[:id].col_value
103
+ @reply=@client.read({},{:method => 'reply'})
104
+ @new_tags=@client.read({},:method => 'new_tags')
105
+ end
106
+ context "reply" do
107
+ should "have author in to field and default title beginning with 're:'" do
108
+ assert_equal 'admin', @reply.schema[:recipients].col_value
109
+ assert_equal 're: ', @reply.schema[:title].col_value[0 .. 3]
110
+ end
111
+ should "accept reply" do
112
+ assert @client.create(@reply.set_values('message' => 'asdf'),{:method => nil,:instance_id => nil})
113
+ end
114
+ end
115
+ context "new tags" do
116
+ should "return tag instance with name tag and same url as original message" do
117
+ assert_equal 'tags', @new_tags.name
118
+ assert_equal "#{@msg.url}tags", @new_tags.url
119
+ end
120
+ should "accept new tags, which should then show up with the original message" do
121
+ assert @client.create({'private_message[tags]' => 'added_tag1, added_tag2'},{:method => 'tags'})
122
+ msg=@client.read({})
123
+ user_tags=msg.find_instances('user_tag')
124
+ assert_same_elements ["inbox", "added_tag1", "added_tag2"], user_tags.map{|t| t.schema[:tag].col_value}
125
+ end
126
+ end
127
+ end
128
+ end
129
+ context "message with 2 tags" do
130
+ setup do
131
+ @client.auth=@admin_auth
132
+ @client.create(@pm.set_values(:recipients => 'user1', :title => 'asdf', :message => 'asdf', :tags => 'test,test2'))
133
+ end
134
+ context "sender (admin) logged in" do
135
+ setup do
136
+ @msg=@client.read[0]
137
+ @tags=@msg.find_instances('user_tag')
138
+ end
139
+ should "have sent tag and both specified tags for sender" do
140
+ assert_same_elements ["sent", "test", "test2"], @tags.map{|t| t.schema[:tag].col_value}
141
+ end
142
+ should "allow sender to remove his own tags'" do
143
+ assert_equal 'remove_parent', @tags[0].delete_action
144
+ assert @client.del({},{:url => @tags[0].url+@msg.url})
145
+ assert_equal 2, @client.read[0].find_instances('user_tag').length
146
+ end
147
+ end
148
+ context "receiver (user1)" do
149
+ setup do
150
+ @client.auth=@user1_auth
151
+ @msg=@client.read[0]
152
+ @tags=@msg.find_instances('user_tag')
153
+ end
154
+ should "have inbox tag and both specified tags" do
155
+ assert_same_elements ["inbox", "test", "test2"], @tags.map{|t| t.schema[:tag].col_value}
156
+ end
157
+ should "have specified tags in reply" do
158
+ reply=@client.read({},{:instance_id => @msg.schema[:id].col_value,:method => 'reply'})
159
+ assert_equal 'test,test2', reply.schema[:tags].col_value
160
+ end
161
+ should "allow receiver to remove his own tags'" do
162
+ assert_equal 'remove_parent', @tags[0].delete_action
163
+ assert @client.del({},{:url => @tags[0].url+@msg.url})
164
+ assert_equal 2, @client.read[0].find_instances('user_tag').length
165
+ end
166
+ end
167
+ context 'user2' do
168
+ should "have no messages" do
169
+ assert resp=@client.read({},{:auth => @user2_auth})
170
+ assert_equal 0, resp.length
171
+ end
172
+ end
173
+ end
174
+ context "message with 2 tags and 2 receivers" do
175
+ setup do
176
+ @client.create(@pm.set_values(:recipients => 'user1,user2',:title => 'asdf',:message => 'asdf', :tags => 'test,test2'),{:auth => @admin_auth})
177
+ end
178
+ should "have sent tag and both specified for sender" do
179
+ resp=@client.read({},{:auth => @admin_auth})
180
+ user_tags=resp[0].find_instances('user_tag')
181
+ assert_same_elements ["sent", "test", "test2"], user_tags.map{|t| t.schema[:tag].col_value}
182
+ end
183
+ should "have inbox tag and both specified for 1st receiver (user1)" do
184
+ resp=@client.read({},{:auth => @user1_auth})
185
+ user_tags=resp[0].find_instances('user_tag')
186
+ assert_same_elements ["inbox", "test", "test2"], user_tags.map{|t| t.schema[:tag].col_value}
187
+ end
188
+ should "have inbox tag and both specified for 2st receiver (user2)" do
189
+ resp=@client.read({},{:auth => @user2_auth})
190
+ user_tags=resp[0].find_instances('user_tag')
191
+ assert_same_elements ["inbox", "test", "test2"], user_tags.map{|t| t.schema[:tag].col_value}
192
+ end
193
+ end
194
+ context "message listing" do
195
+ setup do
196
+ #3 messages with tag "test" for user 1
197
+ @client.create(@pm.set_values(:'recipients' => 'user1',:'title' => 'title1',:'message' => 'body1', :'tags' => 'test'),{:auth => @admin_auth})
198
+ @client.create(@pm.set_values(:'recipients' => 'user1',:'title' => 'title2',:'message' => 'body2', :'tags' => 'test'),{:auth => @admin_auth})
199
+ @client.create(@pm.set_values(:'recipients' => 'user1',:'title' => 'title3',:'message' => 'body3', :'tags' => 'test'),{:auth => @admin_auth})
200
+ #2 messages with tag "test1" for user1 and user2
201
+ @client.create(@pm.set_values(:'recipients' => 'user2,user1',:'title' => 'title1',:'message' => 'body1', :'tags' => 'test1'),{:auth => @admin_auth})
202
+ @client.create(@pm.set_values(:'recipients' => 'user2,user1',:'title' => 'title2',:'message' => 'body2', :'tags' => 'test1'),{:auth => @admin_auth})
203
+ end
204
+ should "for sender (admin) show 3 messages with 'test' tag,2 messages with 'test1' tag, and 5 messages with 'sent' tag" do
205
+ @client.auth=@admin_auth
206
+ assert_equal 3, @client.read({:'private_message[tags]' => 'test'}).length
207
+ assert_equal 2, @client.read({:'private_message[tags]' => 'test1'}).length
208
+ assert_equal 5, @client.read({:'private_message[tags]' => 'sent'}).length
209
+ end
210
+ should "for user1 show 3 messages with 'test' tag, 2 messages with 'test1' tag, 5 messages with 'inbox' tag, and 5 messages with 'test' or 'test1' tags" do
211
+ @client.auth=@user1_auth
212
+ assert_equal 3, @client.read({:'private_message[tags]' => 'test'}).length
213
+ assert_equal 2, @client.read({:'private_message[tags]' => 'test1'}).length
214
+ assert_equal 5, @client.read({:'private_message[tags]' => 'inbox'}).length
215
+ assert_equal 5, @client.read({:'private_message[tags]' => 'test,test1'}).length
216
+ end
217
+ should "for user2 show 0 messages with 'test' tag, 2 messages with 'test1' tag, 2 messages with 'inbox' tag and 2 messages with 'test' or 'test1' tags" do
218
+ @client.auth=@user2_auth
219
+ assert_equal 0, @client.read({:'private_message[tags]' => 'test'}).length
220
+ assert_equal 2, @client.read({:'private_message[tags]' => 'test1'}).length
221
+ assert_equal 2, @client.read({:'private_message[tags]' => 'inbox'}).length
222
+ assert_equal 2, @client.read({:'private_message[tags]' => 'test,test1'}).length
223
+ end
224
+ end
225
+ end
226
+ context "Posts" do
227
+ setup do
228
+ @client.resource_name='post'
229
+ @post=MR::Post.new.to_a
230
+ end
231
+ context 'validation' do
232
+ should "get a validation error trying to post without a title or message as admin, user1, or user2" do
233
+ resp=@client.create({},{:code => 400,:auth => @admin_auth})
234
+ assert_equal :error, resp.resource_type
235
+ assert_equal "validation", resp.error_type
236
+ assert_equal ["is required"], resp.error_details[:title]
237
+ assert_equal ["is required"], resp.error_details[:message]
238
+ user1_resp=@client.create({},{:code => 400,:auth => @user1_auth})
239
+ assert_equal user1_resp, resp
240
+ user2_resp=@client.create({},{:code => 400,:auth => @user2_auth})
241
+ assert_equal user2_resp, resp
242
+ end
243
+ should "be able to post with title and message as admin, user1, or user2" do
244
+ assert @client.create(@post.set_values('title' => 'test', 'message' => 'asdf'),{:auth => @admin_auth})
245
+ assert_equal 1, @client.read({},{:auth => @user1_auth}).length
246
+ assert @client.create(@post.set_values('title' => 'test', 'message' => 'asdf'),{:auth => @user1_auth})
247
+ assert_equal 2, @client.read({},{:auth => @user2_auth}).length
248
+ assert @client.create(@post.set_values('title' => 'test', 'message' => 'asdf'),{:auth => @user2_auth})
249
+ assert_equal 3, @client.read({},{:auth => @admin_auth}).length
250
+ end
251
+ end
252
+ should 'list posts by public tags' do
253
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'admintag1,admintag2'),{:auth => @admin_auth})
254
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'admintag1,admintag2,admintag3'),{:auth => @admin_auth})
255
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'user1tag1'),{:auth => @user1_auth})
256
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'user1tag1,user1tag2'),{:auth => @user1_auth})
257
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'user1tag1,user1tag2,user1tag3'),{:auth => @user1_auth})
258
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'user2tag1,user2tag2,user2tag3'),{:auth => @user2_auth})
259
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'user2tag1,user2tag2,user2tag3,user2tag4'),{:auth => @user2_auth})
260
+ assert_equal 7, @client.read({},{:auth => @admin_auth}).length
261
+ assert_equal 7, @client.read({'post[title]' => 'test'},{:auth => @admin_auth}).length
262
+ assert_equal 7, @client.read({'post[title]' => 'test'},{:auth => @user1_auth}).length
263
+ assert_equal 7, @client.read({'post[title]' => 'test'},{:auth => @user2_auth}).length
264
+ assert_equal 2, @client.read({'post[tags]' => 'admintag1'},{:auth => @admin_auth}).length
265
+ assert_equal 2, @client.read({'post[tags]' => 'admintag1'},{:auth => @user1_auth}).length
266
+ assert_equal 1, @client.read({'post[tags]' => 'admintag3'},{:auth => @user2_auth}).length
267
+ assert_equal 1, @client.read({'post[tags]' => 'admintag3'},{:auth => @admin_auth}).length
268
+ assert_equal 2, @client.read({'post[tags]' => 'user1tag2'},{:auth => @admin_auth}).length
269
+ assert_equal 3, @client.read({'post[tags]' => 'user1tag1'},{:auth => @admin_auth}).length
270
+ assert_equal 3, @client.read({'post[tags]' => 'user1tag1'},{:auth => @user2_auth}).length
271
+ end
272
+ should 'have usable reply, new_tags, and new_user_tags instance actions' do
273
+ @client.create(@post.set_values('title' => 'test', 'message' => 'asdf','tags' => 'admintag1,admintag2'),{:auth => @admin_auth})
274
+ @client.auth=@user2_auth
275
+ posts=@client.read({})
276
+ assert_same_elements ['reply','new_tags','new_user_tags'], posts[0].get_actions
277
+ reply=@client.read({},{:instance_id => posts[0].schema[:id].col_value,:method => 'reply'})
278
+ tags=@client.read({},{:instance_id => posts[0].schema[:id].col_value,:method => 'new_tags'})
279
+ user_tags=@client.read({},{:instance_id => posts[0].schema[:id].col_value,:method => 'new_user_tags'})
280
+ assert_equal 're: test', reply.schema[:title].col_value
281
+ assert @client.create(reply.set_values('message' => 'asdf'),{:method => nil,:instance_id => nil})
282
+ assert @client.create(tags.to_params.merge('post[tags]' => '1,2,3'),{:url => tags.url})
283
+ assert_same_elements ['1','2','3','admintag1','admintag2'], @client.read[0].find_instances('public_tag').map{|t| t.schema[:tag].col_value}
284
+ assert @client.create(user_tags.to_params.merge('post[user_tags]' => '4,5,6'),{:url => user_tags.url})
285
+ assert_same_elements ['4','5','6'], @client.read[0].find_instances('user_tag').map{|t| t.schema[:tag].col_value}
286
+ assert_equal [], @client.read({},{:auth => @user1_auth})[0].find_instances('user_tag').map{|t| t.schema[:tag].col_value}
287
+ end
288
+ end
289
+ end
data/test/test.sqlite3 ADDED
Binary file
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'sequel'
5
+ require 'marley'
6
+ require 'marley/test_helpers'
7
+
8
+ `cp test/empty.sqlite3 test/test.sqlite3`
9
+ DB=Sequel.sqlite("test/test.sqlite3")
10
+
11
+ RESERVED_PM_TAGS=['inbox','sent']
12
+ RESERVED_POST_TAGS=['announcement']
13
+
14
+ Marley.joint 'tagged_messaging'
15
+ Marley.joint 'basic_menu_system'
16
+
@@ -0,0 +1,72 @@
1
+
2
+ class UserTests < Test::Unit::TestCase
3
+ def setup
4
+ MR::User.delete
5
+ @client=Marley::TestClient.new(:resource_name => 'user',:code => 200)
6
+ @user=@client.read({},:resource_name => '',:code => 200).navigation[1]
7
+ end
8
+ should "return login form with no params" do
9
+ assert @client.read({},{:resource_name => ''})
10
+ end
11
+ should "not allow access to menus, private messages, or posts" do
12
+ @client.code=401
13
+ assert @client.read({:resource_name =>'pm_menu'})
14
+ assert @client.read({:resource_name =>'post_menu'})
15
+ assert @client.read({:resource_name =>'private_message'})
16
+ assert @client.read({:resource_name =>'post'})
17
+ end
18
+ should "validate new user properly" do
19
+ @client.code=400
20
+ resp=@client.create(@user)
21
+ assert_equal :error, resp.resource_type
22
+ assert_equal "validation", resp.error_type
23
+ assert_equal ["is required"], resp.error_details[:name]
24
+ @user.col_value(:name,'asdf')
25
+ resp=@client.create(@user)
26
+ assert_equal :error, resp.resource_type
27
+ assert_equal "validation", resp.error_type
28
+ @user.col_value(:password,'asdfaf')
29
+ resp=@client.create(@user)
30
+ assert_equal :error, resp.resource_type
31
+ assert_equal "validation", resp.error_type
32
+ assert_equal ["Password must contain at least 8 characters"], resp.error_details[:password]
33
+ @user.col_value(:password,'asdfaasdf')
34
+ resp=@client.create(@user)
35
+ assert_equal :error, resp.resource_type
36
+ assert_equal "validation", resp.error_type
37
+ assert_equal ["Passwords do not match"], resp.error_details[:confirm_password]
38
+ @user.col_value(:confirm_password,'asdfaasdf')
39
+ assert @client.create(@user,{:code => 201})
40
+ assert @client.create(@user)
41
+ end
42
+ context "existing user logged in" do
43
+ setup do
44
+ @client.code=201
45
+ assert @client.create(:'user[name]' => 'user1',:'user[password]' => 'asdfasdf',:'user[confirm_password]' => 'asdfasdf')
46
+ assert @client.create(:'user[name]' => 'user2',:'user[password]' => 'asdfasdf',:'user[confirm_password]' => 'asdfasdf')
47
+ @client.code=200
48
+ @client.auth=['user1','asdfasdf']
49
+ end
50
+ should "show correct menu items" do
51
+ menu= @client.read({},:resource_name => '')
52
+ assert_same_elements ["User Info","Private Messages","Public Posts"], menu.navigation.map{|n| n.title}
53
+ end
54
+ should "allow viewing and changing of user columns with proper validation" do
55
+ @client.instance_id=1
56
+ assert user=@client.read({})
57
+ assert @client.update(user,{:code => 204})
58
+ user.col_value(:password, 'zxcvzxcv')
59
+ assert err=@client.update(user,{:code => 400})
60
+ assert_equal :error, err.resource_type
61
+ assert_equal "validation", err.error_type
62
+ user.col_value(:confirm_password, 'zxcvzxcv')
63
+ user.col_value(:old_password, 'asdfasdf')
64
+ assert @client.update(user,:code => 204)
65
+ assert @client.read({},:code => 401)
66
+ @client.auth=['user1','zxcvzxcv']
67
+ assert @client.read({})
68
+ @client.instance_id=2
69
+ assert @client.update(user,:code => 403)
70
+ end
71
+ end
72
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marley
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Herb Daily
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2011-11-09 00:00:00 -03:00
12
+ date: 2011-11-18 00:00:00 -03:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -51,16 +51,21 @@ extensions: []
51
51
  extra_rdoc_files: []
52
52
 
53
53
  files:
54
- - marley-0.1.gem
55
54
  - marley.gemspec
55
+ - Rakefile
56
56
  - Favicon.ico
57
57
  - README.rdoc
58
58
  - marley-0.1.0.gem
59
59
  - TODO
60
60
  - reggae.ebnf
61
+ - forum_tests.rb
61
62
  - lib/marley.rb
62
- - lib/test_helpers.rb
63
- - lib/sequel_plugins.rb
63
+ - lib/marley/test_helpers.rb
64
+ - lib/marley/joint.rb
65
+ - lib/marley/controllers.rb
66
+ - lib/marley/reggae.rb
67
+ - lib/sequel/plugins/rest_convenience.rb
68
+ - lib/sequel/plugins/rest_auth.rb
64
69
  - lib/client/jamaica.css
65
70
  - lib/client/jquery-1.6.2.js
66
71
  - lib/client/jamaica.rb
@@ -72,18 +77,21 @@ files:
72
77
  - lib/joints/user_based_navigation.rb
73
78
  - lib/joints/basic_menu_system.rb
74
79
  - lib/joints/tagged_messaging.rb
75
- - lib/joint.rb
76
- - lib/controllers.rb
77
- - lib/reggae.rb
78
80
  - examples/forum.js
79
81
  - examples/forum.sql
82
+ - examples/blog.rb
80
83
  - examples/forum.rb
81
84
  - examples/forum.sqlite3
82
85
  - examples/forum_test.sqlite3
83
86
  - examples/empty.sqlite3
84
87
  - examples/forum.css
85
88
  - examples/run.sh
86
- - test/forum_tests.rb
89
+ - test/menu_tests.rb
90
+ - test/test_include.rb
91
+ - test/empty.sqlite3
92
+ - test/user_tests.rb
93
+ - test/test.sqlite3
94
+ - test/tagged_messaging_tests.rb
87
95
  has_rdoc: true
88
96
  homepage: http://github.com/herbdaily/marley
89
97
  licenses: []
data/marley-0.1.gem DELETED
Binary file