crudtree 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +46 -0
- data/lib/crudtree.rb +3 -0
- data/lib/crudtree/generator.rb +87 -0
- data/lib/crudtree/helper.rb +19 -0
- data/lib/crudtree/interface.rb +14 -0
- data/lib/crudtree/interface/usher.rb +43 -0
- data/lib/crudtree/interface/usher/rack.rb +40 -0
- data/lib/crudtree/tree.rb +3 -0
- data/lib/crudtree/tree/endnode.rb +44 -0
- data/lib/crudtree/tree/master.rb +20 -0
- data/lib/crudtree/tree/node.rb +129 -0
- data/test/helper/suite/lib/generator.rb +44 -0
- data/test/helper/suite/lib/integration.rb +9 -0
- data/test/helper/suite/lib/interface/blackbox.rb +36 -0
- data/test/setup.rb +6 -0
- data/test/suite/lib/generator.rb +244 -0
- data/test/suite/lib/integration.rb +33 -0
- data/test/suite/lib/interface/blackbox.rb +132 -0
- data/test/suite/lib/interface/usher.rb +55 -0
- data/test/suite/lib/interface/usher/rack.rb +90 -0
- data/test/suite/lib/tree/endnode.rb +27 -0
- data/test/suite/lib/tree/master.rb +9 -0
- data/test/suite/lib/tree/node.rb +91 -0
- metadata +77 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Simon Hafner aka Tass
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
= CRUDtree
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
A resource helper mainly for usher, but may be adapted for other routers as
|
6
|
+
well.
|
7
|
+
|
8
|
+
== See
|
9
|
+
|
10
|
+
Usher:: http://github.com/joshbuddy/usher
|
11
|
+
IRC:: #rango@irc.freenode.net
|
12
|
+
Baretest:: http://github.com/apeiros/baretest
|
13
|
+
|
14
|
+
== Terminology
|
15
|
+
|
16
|
+
Master:: The main body, only one per Usher instance as well.
|
17
|
+
Node:: You attach other Nodes or EndNodes as subnodes here.
|
18
|
+
EndNode:: A route endpoint.
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
=== as Tinkerer
|
23
|
+
|
24
|
+
require 'crudtree/interface/usher/rack'
|
25
|
+
|
26
|
+
Usher::Interface.for(:rack) do
|
27
|
+
extend CRUDtree::Interface::Usher::Rack
|
28
|
+
node(klass: Posts, model: Post) do
|
29
|
+
sub(type: :member, call: :show, rest: :get)
|
30
|
+
sub(type: :collection, call: :index, rest: :get)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
=== As Dev
|
35
|
+
|
36
|
+
require 'crudtree/interface/usher/rack'
|
37
|
+
require 'crudtree/helper'
|
38
|
+
|
39
|
+
Usher::Interface.for(:rack) do
|
40
|
+
extend CRUDtree::Interface::Usher::Rack
|
41
|
+
extend CRUDtree::Interface::Helper
|
42
|
+
resource(klass: Posts, model: Post) do # the resource helper will include a bunch of default routes
|
43
|
+
member(call: :show, rest: :get)
|
44
|
+
collection(call: :index, rest: :get)
|
45
|
+
end
|
46
|
+
end
|
data/lib/crudtree.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module CRUDtree
|
2
|
+
class Generator
|
3
|
+
def initialize(master)
|
4
|
+
@master = master
|
5
|
+
@model_to_node = {}
|
6
|
+
@master.nodes.each {|node| add_node_models(node) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate(resource, *names)
|
10
|
+
if resource.is_a? Symbol
|
11
|
+
names.unshift(resource)
|
12
|
+
node = @master
|
13
|
+
url = ""
|
14
|
+
else
|
15
|
+
node, identifiers = find_node(resource)
|
16
|
+
last_identifier = identifiers.last
|
17
|
+
url = generate_url_from_node(node, identifiers)
|
18
|
+
end
|
19
|
+
generate_from_sub(node, names, url, last_identifier) unless names.empty?
|
20
|
+
url
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def find_node(resource)
|
25
|
+
case nodes = @model_to_node[resource.class]
|
26
|
+
when Node
|
27
|
+
[nodes, [resource.send(nodes.identifier)]]
|
28
|
+
when Array
|
29
|
+
valid_nodes = {}
|
30
|
+
nodes.each do |node|
|
31
|
+
identifiers = identifiers_to_node(resource, node)
|
32
|
+
valid_nodes[node] = identifiers if identifiers
|
33
|
+
end
|
34
|
+
parents = valid_nodes.keys.map(&:parents).flatten
|
35
|
+
valid_nodes.reject!{|node, identifiers| parents.include?(node)}
|
36
|
+
case valid_nodes.size
|
37
|
+
when 1
|
38
|
+
valid_nodes.first
|
39
|
+
when 0
|
40
|
+
raise(NoNode, "No node found for #{resource}.")
|
41
|
+
else
|
42
|
+
raise(NoUniqueNode, "No unique node found for #{resource}.")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_node_models(node)
|
48
|
+
@model_to_node[node.model] = if target_node = @model_to_node[node.model]
|
49
|
+
([target_node] << node).flatten
|
50
|
+
else
|
51
|
+
node
|
52
|
+
end
|
53
|
+
node.nodes.each { |subnode|
|
54
|
+
add_node_models(subnode)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_url_from_node(node, identifiers)
|
59
|
+
(node.parents.reverse + [node]).map {|parent|
|
60
|
+
"/#{parent.path}/#{identifiers.shift}"
|
61
|
+
}.join
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Array] with [String] of identifiers or false if the model is invalid
|
65
|
+
# for this node
|
66
|
+
def identifiers_to_node(model, node, identifiers = [])
|
67
|
+
return false unless node.model == model.class
|
68
|
+
identifiers << model.send(node.identifier).to_s
|
69
|
+
unless node.parent_is_master?
|
70
|
+
identifiers_to_node(model.send(node.parent_call), node.parent, identifiers) or return false
|
71
|
+
end
|
72
|
+
identifiers.reverse
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_from_sub(node, names, url, last_identifier=nil)
|
76
|
+
name = names.shift
|
77
|
+
sub = node.subs.find{|sub| sub.name == name} or raise(ArgumentError, "No subnode found on #{node} with name of #{name}.")
|
78
|
+
url.chomp!("/#{last_identifier}") if last_identifier && sub.collection?
|
79
|
+
url << "/#{sub.path}"
|
80
|
+
generate_from_sub(sub, names, url) unless names.empty?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class InvalidPath < StandardError; end
|
85
|
+
class NoUniqueNode < StandardError; end
|
86
|
+
class NoNode < StandardError; end
|
87
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CRUDtree
|
2
|
+
module Interface
|
3
|
+
module Helper
|
4
|
+
def resource(params, &resource_block)
|
5
|
+
node(params) do
|
6
|
+
collection(call: :index, rest: :get)
|
7
|
+
collection(call: :new, rest: :get)
|
8
|
+
collection(call: :create, rest: :post, path: "")
|
9
|
+
member(call: :show, rest: :get)
|
10
|
+
member(call: :edit, rest: :get)
|
11
|
+
member(call: :update, rest: :put, path: "")
|
12
|
+
member(call: :delete, rest: :get)
|
13
|
+
member(call: :destroy, rest: :delete, path: "")
|
14
|
+
instance_eval(&resource_block) if resource_block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative "../interface"
|
2
|
+
module CRUDtree
|
3
|
+
module Interface
|
4
|
+
module Usher
|
5
|
+
# Integration part
|
6
|
+
|
7
|
+
def master_params
|
8
|
+
master.params
|
9
|
+
end
|
10
|
+
|
11
|
+
def master
|
12
|
+
@master ||= Master.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Logic part
|
16
|
+
|
17
|
+
def node(params, &block)
|
18
|
+
node = master.node(params, &block)
|
19
|
+
compile_node("", node)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def compile_node(pre_path, node)
|
24
|
+
paths = node.paths.map {|p| "#{pre_path}/#{p}"}
|
25
|
+
paths.each do |path|
|
26
|
+
node.subnodes.each do |subnode|
|
27
|
+
compile_subnode(path, subnode)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile_subnode(pre_path, subnode)
|
33
|
+
case subnode
|
34
|
+
when EndNode
|
35
|
+
compile_endnode(pre_path, subnode)
|
36
|
+
when Node
|
37
|
+
compile_node("#{pre_path}/:#{subnode.identifier}", subnode)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../usher'
|
2
|
+
|
3
|
+
module CRUDtree
|
4
|
+
module Interface
|
5
|
+
module Usher
|
6
|
+
module Rack
|
7
|
+
include CRUDtree::Interface::Usher
|
8
|
+
|
9
|
+
def self.attach
|
10
|
+
::Usher::Interface.class_for(:rack).send(:include, self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.add_helper(helper)
|
14
|
+
include helper
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def compile_endnode(pre_path, endnode)
|
19
|
+
conditions = {}
|
20
|
+
conditions.merge!({request_method: endnode.rest.to_s.upcase}) if endnode.rest
|
21
|
+
method_call = [endnode.call]
|
22
|
+
method_call.unshift(master_params[:dispatcher])
|
23
|
+
# Here we call usher.
|
24
|
+
path(compile_path(pre_path, endnode), conditions: conditions).to(endnode.parent.klass.send(*method_call))
|
25
|
+
end
|
26
|
+
|
27
|
+
def compile_path(pre_path, endnode)
|
28
|
+
node = endnode.parent
|
29
|
+
compiled_path = [pre_path]
|
30
|
+
compiled_path << ":#{node.identifier}" if endnode.type == :member
|
31
|
+
compiled_path << "#{endnode.path}" unless endnode.path.empty?
|
32
|
+
compiled_path.join('/')
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
CRUDtree::Interface.register(:usher_rack, CRUDtree::Interface::Usher::Rack)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CRUDtree
|
2
|
+
class EndNode
|
3
|
+
# The params Hash takes the following keys:
|
4
|
+
#
|
5
|
+
# :type
|
6
|
+
# may either be member or collection
|
7
|
+
#
|
8
|
+
# :path
|
9
|
+
# path to this endnode - you can use
|
10
|
+
# join
|
11
|
+
# or
|
12
|
+
# join/:date/:foo/:whatever
|
13
|
+
# the interface will handle those parameters.
|
14
|
+
# Defaults to call.to_s
|
15
|
+
#
|
16
|
+
# :rest
|
17
|
+
# which REST method should match this route. Defaults to nil, aka all.
|
18
|
+
#
|
19
|
+
# :call
|
20
|
+
# The method to be called if this route is matched. Required.
|
21
|
+
#
|
22
|
+
# :name
|
23
|
+
# The name of this route, used for generating. Symbol.
|
24
|
+
# Defaults to call
|
25
|
+
#
|
26
|
+
def initialize(parent, params)
|
27
|
+
@type = params[:type] if [:member, :collection].include? params[:type]
|
28
|
+
raise ArgumentError, "Invalid type: #{params[:type]}" unless @type
|
29
|
+
@call = params[:call] or raise ArgumentError, "No call given."
|
30
|
+
@path = params[:path] || @call.to_s
|
31
|
+
raise ArgumentError, "No path given." unless @path
|
32
|
+
@rest = params[:rest]
|
33
|
+
@name = params[:name] || @call
|
34
|
+
@parent = parent
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :type, :path, :rest, :call, :name, :parent
|
38
|
+
|
39
|
+
def collection?
|
40
|
+
type == :collection
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CRUDtree
|
2
|
+
class Master
|
3
|
+
|
4
|
+
# Use :rango => true if you're using rango
|
5
|
+
def initialize(params = {})
|
6
|
+
@nodes = []
|
7
|
+
@params = {dispatcher: :dispatcher}.merge(params)
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :nodes
|
11
|
+
attr_accessor :mapping, :params
|
12
|
+
|
13
|
+
alias_method :subs, :nodes
|
14
|
+
|
15
|
+
def node(params, &block)
|
16
|
+
@nodes << new_node = Node.new(self, params, &block)
|
17
|
+
new_node
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module CRUDtree
|
2
|
+
class Node
|
3
|
+
# The params Hash takes the following keys:
|
4
|
+
#
|
5
|
+
# :klass
|
6
|
+
# The object where to send the method that is returned by the router.
|
7
|
+
# Mostly a class, therefore it's called 'class_name'. Defaults to nil,
|
8
|
+
# but the interface may complain. You have been warned ;).
|
9
|
+
# May be used by the interface as needed (Rango wants Class.send
|
10
|
+
# :dispatcher, :send_method)
|
11
|
+
#
|
12
|
+
# :identifier
|
13
|
+
# The identifier used to identify a resource, aka /user/:name instead of
|
14
|
+
# /user/:id (default).
|
15
|
+
#
|
16
|
+
# :default_collection
|
17
|
+
# The collection that is called when nothing is given. Defaults to :index.
|
18
|
+
#
|
19
|
+
# :default_member
|
20
|
+
# The member which is chosen when no method is given. Defaults to :show.
|
21
|
+
#
|
22
|
+
# :paths
|
23
|
+
# Specify the path(s) you want to call this resource with.
|
24
|
+
# Defaults to klass.to_s.downcase
|
25
|
+
# The first one is used for generation.
|
26
|
+
#
|
27
|
+
# Options used for generating
|
28
|
+
#
|
29
|
+
# :model
|
30
|
+
# The name of the model. Needed. We don't do magic here.
|
31
|
+
#
|
32
|
+
# :parent_call
|
33
|
+
# The method to call on the model object to get its parent (for nested
|
34
|
+
# resources). Defaults to :model.downcase of the parent.
|
35
|
+
#
|
36
|
+
# :name
|
37
|
+
# Symbol used to identify the node when generating a collection route.
|
38
|
+
# Defaults to Klass.to_s.downcase.to_sym
|
39
|
+
#
|
40
|
+
def initialize(parent, params, &block)
|
41
|
+
@klass = params[:klass]
|
42
|
+
@identifier = params[:identifier] || :id
|
43
|
+
@default_collection = params[:default_collection] || :index
|
44
|
+
@default_member = params[:default_member] || :show
|
45
|
+
@paths = if params[:paths]
|
46
|
+
[params[:paths]].flatten
|
47
|
+
elsif params[:klass]
|
48
|
+
[params[:klass].to_s.downcase.split("::").last]
|
49
|
+
else
|
50
|
+
raise ArgumentError, "No paths given"
|
51
|
+
end
|
52
|
+
@subnodes = []
|
53
|
+
@parent = parent
|
54
|
+
# default routes
|
55
|
+
@subnodes.unshift(EndNode.new(self, type: :member, call: :show, path: "", rest: :get))
|
56
|
+
@subnodes.unshift(EndNode.new(self, type: :collection, call: :index, path: "", rest: :get))
|
57
|
+
# generating
|
58
|
+
unless @model = params[:model]
|
59
|
+
raise(ArgumentError, "No model given.")
|
60
|
+
end
|
61
|
+
@parent_call = if params[:parent_call]
|
62
|
+
params[:parent_call]
|
63
|
+
elsif ! parent_is_master?
|
64
|
+
parent.model.to_s.split('::').last.downcase
|
65
|
+
else
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
@name = params[:name] || klass.to_s.downcase.to_sym
|
69
|
+
block ? instance_eval(&block) : raise(ArgumentError, "No block given.")
|
70
|
+
end
|
71
|
+
|
72
|
+
attr_reader :klass, :identifier, :default_collection, :default_member, :paths, :parent, :subnodes, :model, :parent_call, :name
|
73
|
+
|
74
|
+
# Creates a new End and attaches it to this Node.
|
75
|
+
def endnode(params)
|
76
|
+
@subnodes << EndNode.new(self, params)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creates a new endnode with type member. See Endnode.
|
80
|
+
def member(params)
|
81
|
+
endnode(params.merge({type: :member}))
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a new endnode with type collection. See EndNode.
|
85
|
+
def collection(params)
|
86
|
+
endnode(params.merge({type: :collection}))
|
87
|
+
end
|
88
|
+
|
89
|
+
def node(params, &block)
|
90
|
+
@subnodes << Node.new(self, params, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def parents
|
94
|
+
[find_parent(self)].flatten[0..-2]
|
95
|
+
end
|
96
|
+
|
97
|
+
def nodes
|
98
|
+
subnodes.select{|subnode| subnode.is_a? Node}
|
99
|
+
end
|
100
|
+
|
101
|
+
def endnodes
|
102
|
+
subnodes.select{|subnode| subnode.is_a? EndNode}
|
103
|
+
end
|
104
|
+
|
105
|
+
def parent_is_master?
|
106
|
+
! parent.respond_to? :parent
|
107
|
+
end
|
108
|
+
|
109
|
+
# Duck typing used for generation
|
110
|
+
def path
|
111
|
+
@paths.first
|
112
|
+
end
|
113
|
+
|
114
|
+
def subs
|
115
|
+
endnodes
|
116
|
+
end
|
117
|
+
|
118
|
+
def collection?
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
def find_parent(node)
|
124
|
+
if node.parent.respond_to? :parent
|
125
|
+
[node.parent, find_parent(node.parent)]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|