marley 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +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
|