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 +48 -21
- data/Rakefile +11 -0
- data/examples/blog.rb +26 -0
- data/examples/forum.sqlite3 +0 -0
- data/examples/forum_test.sqlite3 +0 -0
- data/{test/forum_tests.rb → forum_tests.rb} +11 -11
- data/lib/joints/basic_menu_system.rb +18 -14
- data/lib/joints/basic_messaging.rb +1 -1
- data/lib/joints/basic_user.rb +1 -1
- data/lib/joints/tagged_messaging.rb +5 -3
- data/lib/{controllers.rb → marley/controllers.rb} +0 -0
- data/lib/{joint.rb → marley/joint.rb} +0 -2
- data/lib/{reggae.rb → marley/reggae.rb} +35 -18
- data/lib/{test_helpers.rb → marley/test_helpers.rb} +7 -3
- data/lib/marley.rb +10 -5
- data/lib/sequel/plugins/rest_auth.rb +53 -0
- data/lib/{sequel_plugins.rb → sequel/plugins/rest_convenience.rb} +3 -53
- data/marley-0.1.0.gem +0 -0
- data/marley.gemspec +1 -1
- data/test/empty.sqlite3 +0 -0
- data/test/menu_tests.rb +9 -0
- data/test/tagged_messaging_tests.rb +289 -0
- data/test/test.sqlite3 +0 -0
- data/test/test_include.rb +16 -0
- data/test/user_tests.rb +72 -0
- metadata +17 -9
- data/marley-0.1.gem +0 -0
data/README.rdoc
CHANGED
@@ -1,48 +1,75 @@
|
|
1
1
|
=Marley
|
2
2
|
|
3
|
-
Marley is a
|
3
|
+
Marley is a framework for quickly building RESTful web services and applications. It consists of several parts:
|
4
4
|
|
5
|
-
* A
|
6
|
-
* A
|
7
|
-
*
|
8
|
-
*
|
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
|
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
|
-
|
20
|
+
==Marley Resources
|
17
21
|
|
18
|
-
|
22
|
+
Marley resources are constants in the Marley::Resources namespace.
|
19
23
|
|
20
|
-
*
|
21
|
-
|
22
|
-
|
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
|
-
|
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.
|
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
|
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
|
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
|
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
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
|
data/examples/forum.sqlite3
CHANGED
Binary file
|
data/examples/forum_test.sqlite3
CHANGED
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
29
|
+
include Sequel::Plugins::RestSection::ClassMethods
|
23
30
|
def self.rest_get
|
24
|
-
new.
|
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
|
-
@
|
35
|
-
@
|
36
|
-
@
|
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
|
-
@
|
39
|
-
@
|
40
|
-
@
|
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
|
data/lib/joints/basic_user.rb
CHANGED
@@ -13,7 +13,7 @@ module Marley
|
|
13
13
|
! ($request[:verb]=='rest_post')
|
14
14
|
end
|
15
15
|
def self.section
|
16
|
-
ReggaeSection.new(
|
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
|
14
|
-
|
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
|
@@ -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].
|
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
|
-
|
54
|
-
|
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
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
115
|
+
super
|
99
116
|
else
|
100
|
-
|
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 { |
|
40
|
-
process(CRUD2REST[op],
|
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 '
|
7
|
-
require '
|
8
|
-
require '
|
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
|
-
|
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(
|
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.
|
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"]
|
data/test/empty.sqlite3
ADDED
Binary file
|
data/test/menu_tests.rb
ADDED
@@ -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
|
+
|
data/test/user_tests.rb
ADDED
@@ -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.
|
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-
|
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/
|
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/
|
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
|