crudtree 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ require_relative 'crudtree/tree'
2
+ require_relative 'crudtree/interface'
3
+ require_relative 'crudtree/generator'
@@ -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,14 @@
1
+ require_relative "tree"
2
+ module CRUDtree
3
+ module Interface
4
+ InterfaceRegistry = {}
5
+
6
+ def self.register(name, mod)
7
+ InterfaceRegistry[name.to_sym] = mod
8
+ end
9
+
10
+ def self.for(name)
11
+ InterfaceRegistry[name.to_sym].attach
12
+ end
13
+ end
14
+ 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,3 @@
1
+ require_relative 'tree/master'
2
+ require_relative 'tree/node'
3
+ require_relative 'tree/endnode'
@@ -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