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 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