crudtree 0.1.2
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/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
|