acts_as_graph_diagram 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +178 -0
  4. data/Rakefile +5 -0
  5. data/bin/test +7 -0
  6. data/lib/acts_as_graph_diagram/edge_scopes.rb +23 -0
  7. data/lib/acts_as_graph_diagram/node/graph_calculator.rb +129 -0
  8. data/lib/acts_as_graph_diagram/node.rb +120 -0
  9. data/lib/acts_as_graph_diagram/railtie.rb +11 -0
  10. data/lib/acts_as_graph_diagram/version.rb +5 -0
  11. data/lib/acts_as_graph_diagram.rb +12 -0
  12. data/lib/generators/USAGE +5 -0
  13. data/lib/generators/acts_as_graph_diagram_generator.rb +30 -0
  14. data/lib/generators/templates/migration.rb +21 -0
  15. data/lib/generators/templates/model.rb +24 -0
  16. data/test/acts_as_graph_diagram_test.rb +44 -0
  17. data/test/dummy/Rakefile +39 -0
  18. data/test/dummy/app/assets/config/manifest.js +3 -0
  19. data/test/dummy/app/assets/images/.keep +0 -0
  20. data/test/dummy/app/assets/stylesheets/application.css +1 -0
  21. data/test/dummy/app/channels/application_cable/channel.rb +6 -0
  22. data/test/dummy/app/channels/application_cable/connection.rb +6 -0
  23. data/test/dummy/app/controllers/application_controller.rb +4 -0
  24. data/test/dummy/app/controllers/concerns/.keep +0 -0
  25. data/test/dummy/app/controllers/gods_controller.rb +67 -0
  26. data/test/dummy/app/helpers/application_helper.rb +4 -0
  27. data/test/dummy/app/helpers/gods_helper.rb +4 -0
  28. data/test/dummy/app/javascript/application.js +94 -0
  29. data/test/dummy/app/jobs/application_job.rb +9 -0
  30. data/test/dummy/app/mailers/application_mailer.rb +6 -0
  31. data/test/dummy/app/models/application_record.rb +5 -0
  32. data/test/dummy/app/models/concerns/.keep +0 -0
  33. data/test/dummy/app/models/edge.rb +24 -0
  34. data/test/dummy/app/models/god.rb +14 -0
  35. data/test/dummy/app/views/gods/_form.html.erb +22 -0
  36. data/test/dummy/app/views/gods/_god.html.erb +16 -0
  37. data/test/dummy/app/views/gods/edit.html.erb +6 -0
  38. data/test/dummy/app/views/gods/index.html.erb +35 -0
  39. data/test/dummy/app/views/gods/new.html.erb +5 -0
  40. data/test/dummy/app/views/gods/show.html.erb +9 -0
  41. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  42. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  43. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  44. data/test/dummy/bin/importmap +5 -0
  45. data/test/dummy/bin/rails +6 -0
  46. data/test/dummy/bin/rake +6 -0
  47. data/test/dummy/bin/setup +35 -0
  48. data/test/dummy/config/application.rb +23 -0
  49. data/test/dummy/config/boot.rb +7 -0
  50. data/test/dummy/config/cable.yml +10 -0
  51. data/test/dummy/config/database.yml +25 -0
  52. data/test/dummy/config/environment.rb +7 -0
  53. data/test/dummy/config/environments/development.rb +71 -0
  54. data/test/dummy/config/environments/production.rb +89 -0
  55. data/test/dummy/config/environments/test.rb +62 -0
  56. data/test/dummy/config/importmap.rb +43 -0
  57. data/test/dummy/config/initializers/content_security_policy.rb +26 -0
  58. data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
  59. data/test/dummy/config/initializers/inflections.rb +17 -0
  60. data/test/dummy/config/initializers/permissions_policy.rb +12 -0
  61. data/test/dummy/config/locales/en.yml +33 -0
  62. data/test/dummy/config/puma.rb +45 -0
  63. data/test/dummy/config/routes.rb +11 -0
  64. data/test/dummy/config/storage.yml +34 -0
  65. data/test/dummy/config.ru +8 -0
  66. data/test/dummy/db/migrate/20220606102242_create_gods.rb +11 -0
  67. data/test/dummy/db/migrate/20220612090334_acts_as_graph_diagram_migration.rb +21 -0
  68. data/test/dummy/db/schema.rb +35 -0
  69. data/test/dummy/db/seeds.rb +53 -0
  70. data/test/dummy/lib/assets/.keep +0 -0
  71. data/test/dummy/log/.keep +0 -0
  72. data/test/dummy/public/404.html +67 -0
  73. data/test/dummy/public/422.html +67 -0
  74. data/test/dummy/public/500.html +66 -0
  75. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  76. data/test/dummy/public/apple-touch-icon.png +0 -0
  77. data/test/dummy/public/favicon.ico +0 -0
  78. data/test/dummy/test/controllers/gods_controller_test.rb +50 -0
  79. data/test/dummy/test/system/gods_test.rb +43 -0
  80. data/test/fixtures/edges.yml +111 -0
  81. data/test/fixtures/gods.yml +81 -0
  82. data/test/test_helper.rb +18 -0
  83. metadata +434 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bcef649a07aef82c036b7c6df58bf00513702a32c5960dc3af5d4088267cc618
4
+ data.tar.gz: fc4a5712c0d0b05f165dc665a324a612cf496ee04eafc69ef587cb1253ee9ecb
5
+ SHA512:
6
+ metadata.gz: 1cb85f7ee1c208c5571fa3d447045028917f92d91bf3b70deead1afa01b06a43f5c795b2b9bf324e3b111879ec966fb9b9ff3bced0806b37a9c3b4a65d7a0d11
7
+ data.tar.gz: c27c720b88ca5ae95f4a4c721f5e625625a49b3ae035e0c140a369cb416aeaf0f72a2411521301203d45a4ecb2f600c5ab7a85697226120dc7774b0e7b7f02aa
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 smapira
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.md ADDED
@@ -0,0 +1,178 @@
1
+ # Acts As Graph Diagram
2
+
3
+ Acts As Graph Diagram extends Active Record to add simple function for draw the Force Directed Graph with html.
4
+
5
+ ![Ruby](https://img.shields.io/badge/Ruby-CC342D?style=for-the-badge&logo=ruby&logoColor=white)
6
+ [![Gem Version](https://badge.fury.io/rb/acts_as_graph_diagram.svg)](https://badge.fury.io/rb/acts_as_graph_diagram)
7
+ ![](https://ruby-gem-downloads-badge.herokuapp.com/acts_as_graph_diagram)
8
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop-hq/rubocop)
9
+ [![CircleCI](https://circleci.com/gh/routeflags/acts_as_graph_diagram.svg?style=svg)](https://circleci.com/gh/routeflags/acts_as_graph_diagram)
10
+
11
+ ## See It Work
12
+
13
+ ![acts_as_graph_diagram](https://user-images.githubusercontent.com/25024587/173231019-ede998c4-333a-48dd-b2da-96f04e1fce86.gif)
14
+
15
+ ## Usage
16
+
17
+ Append the line to your model file like below:
18
+ ```ruby
19
+ class God < ApplicationRecord
20
+ acts_as_graph_diagram
21
+ end
22
+
23
+ God.find_by(name: 'Rheā').add_destination God.find_by(name: 'Hēra', cost: 1)
24
+ # => #<Edge:0x000000010b0d4560
25
+ # id: 1,
26
+ # comment: "",
27
+ # cost: 0,
28
+ # directed: true,
29
+ # destination_type: "God",
30
+ # destination_id: 2,
31
+ # departure_type: "God",
32
+ # departure_id: 1,
33
+ # created_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00,
34
+ # updated_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00>
35
+
36
+ God.find_by(name: 'Rheā').connecting_count
37
+ # => 1
38
+
39
+ God.find_by(name: 'Rheā').destinations
40
+ # => [#<Edge:0x000000010b5642b0
41
+ # id: 1,
42
+ # comment: "",
43
+ # cost: 0,
44
+ # directed: true,
45
+ # destination_type: "God",
46
+ # destination_id: 2,
47
+ # departure_type: "God",
48
+ # departure_id: 1,
49
+ # created_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00,
50
+ # updated_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00>]
51
+
52
+ God.find_by(name: 'Rheā').aheads.first.destination
53
+ # => #<God:0x000000010b5efb58 id: 2, name: "Hēra", created_at: Sun, 12 Jun 2022 11:11:06.984341000 UTC +00:00, updated_at: Sun, 12 Jun 2022 11:11:06.984341000 UTC +00:00>
54
+ ```
55
+
56
+ ### Methods
57
+
58
+ * aheads
59
+ * behinds
60
+ * add_destination(node, comment: '', cost: 0)
61
+ * add_departure(node, comment: '', cost: 0)
62
+ * get_destination(node)
63
+ * get_departure(node)
64
+ * remove_destination(node)
65
+ * remove_departure(node)
66
+ * connecting?(node)
67
+ * connecting_count()
68
+ * add_connection(node, directed: false, comment: '', cost: 0)
69
+ * sum_cost()
70
+ * sum_tree_cost()
71
+ * assemble_tree_nodes()
72
+
73
+ ### Draws the graph diagram with D3.js
74
+
75
+ 1. Append the lines to your controller file like below:
76
+ ```ruby
77
+ class GodsController < ApplicationController
78
+ def data_network
79
+ render json: { 'nodes' => God.all.pluck(:id, :name)
80
+ .map { |x| Hash[id: x[0], name: x[1]] },
81
+ 'links' => Edge.all.pluck(:destination_id, :departure_id)
82
+ .map { |x| Hash[target: x[0], source: x[1]] } }
83
+ end
84
+ end
85
+ ```
86
+
87
+ 2. And append the line to your routes.rb file like below:
88
+ ```ruby
89
+ Rails.application.routes.draw do
90
+ get 'data_network' => 'gods#data_network'
91
+ end
92
+ ```
93
+
94
+ 3. Then append the line to your javascript file like below:
95
+ ```javascript
96
+ // v7.4.4
97
+ d3.json("http://127.0.0.1:3000/data_network").then(function (graph) {});
98
+ ```
99
+
100
+ ### Calculates the Program Evaluation and Review Technique (PERT)
101
+
102
+ ![Pert_chart_colored](https://user-images.githubusercontent.com/25024587/174105277-213a955a-b783-43ae-be98-1174d9256273.gif)
103
+
104
+ > [PERT Chart. Drawn in Adobe Illustrator - inspired by a chart at netmba.com. Created by Jeremy Kemp. 2005/01/11 From Wikipedia, the free encyclopedia](https://en.wikipedia.org/wiki/Program_evaluation_and_review_technique)
105
+
106
+ ```ruby
107
+ Milestone.create(name: 10)
108
+ Milestone.create(name: 20)
109
+ Milestone.create(name: 30)
110
+ Milestone.create(name: 40)
111
+ Milestone.create(name: 50)
112
+
113
+ Milestone.find_by(name: 10).add_destination(Milestone.find_by(name: 20), cost: 3)
114
+ Milestone.find_by(name: 10).add_destination(Milestone.find_by(name: 30), cost: 4)
115
+ Milestone.find_by(name: 30).add_destination(Milestone.find_by(name: 40), cost: 1)
116
+ Milestone.find_by(name: 40).add_destination(Milestone.find_by(name: 50), cost: 3)
117
+ Milestone.find_by(name: 30).add_destination(Milestone.find_by(name: 50), cost: 2)
118
+ Milestone.find_by(name: 20).add_destination(Milestone.find_by(name: 50), cost: 3)
119
+
120
+ Milestone.find_by(name: 10).sum_tree_cost
121
+
122
+ # => 16
123
+ ```
124
+
125
+ ## Installation
126
+ Add this line to your application's Gemfile:
127
+
128
+ ```ruby
129
+ gem "acts_as_graph_diagram"
130
+ ```
131
+
132
+ And then execute:
133
+ ```bash
134
+ $ bundle
135
+ $ bin/rails generate acts_as_graph_diagram
136
+ $ bin/rails db:migrate
137
+ ```
138
+
139
+ Or install it yourself as:
140
+ ```bash
141
+ $ gem install acts_as_graph_diagram
142
+ ```
143
+
144
+ ## Development
145
+ ### Rails console
146
+ ```bash
147
+ test/dummy/bin/rails console
148
+ ```
149
+
150
+ ### Test
151
+ ```bash
152
+ bin/test
153
+ ```
154
+
155
+ ## Contributing
156
+ Bug reports and pull requests are welcome on Github at https://github.com/routeflags/acts_as_graph_diagram. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
157
+
158
+ ## License
159
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
160
+
161
+ ## Changelog
162
+ available [here](https://github.com/routeflags/acts_as_graph_diagram/main/CHANGELOG.md).
163
+
164
+ ## Code of Conduct
165
+ Everyone interacting in the ActsAsTreeDiagram project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/routeflags/acts_as_graph_diagram/main/CODE_OF_CONDUCT.md).
166
+
167
+ ## You may enjoy owning other libraries and my company.
168
+
169
+ * [acts_as_graph_diagram: ActsAsTreeDiagram extends ActsAsTree to add simple function for draw tree diagram with html.](https://github.com/routeflags/acts_as_graph_diagram)
170
+ * [timeline_rails_helper: The TimelineRailsHelper provides a timeline_molecules_tag helper to draw a vertical time line usable with vanilla CSS.](https://github.com/routeflags/timeline_rails_helper)
171
+ * [株式会社旗指物](https://blog.routeflags.com/)
172
+
173
+ ## Аcknowledgments
174
+
175
+ - [activerecord - Model an undirected graph in Rails? - Stack Overflow](https://stackoverflow.com/questions/7976301/model-an-undirected-graph-in-rails)
176
+ - [tcocca/acts_as_follower: A Gem to add Follow functionality for models](https://github.com/tcocca/acts_as_follower)
177
+ - [Force layout | D3 in Depth](https://www.d3indepth.com/force-layout/)
178
+ - [Rubyを使って「なぜ関数プログラミングは重要か」を読み解く(改定)─ 前編 ─ 但し後編の予定なし](https://melborne.github.io/2013/01/21/why-fp-with-ruby/)
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
data/bin/test ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path('../test', __dir__)
5
+
6
+ require 'bundler/setup'
7
+ require 'rails/plugin/test'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsGraphDiagram # :nodoc:
4
+ ##
5
+ # This module represents a act of edge.
6
+ module EdgeScopes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # returns Edge records where destination is the record passed in.
11
+ # @param [Node] node
12
+ scope :select_destinations, ->(node) { where(destination_id: node.id, destination_type: node.class.name) }
13
+
14
+ # returns Edge records where departure is the record passed in.
15
+ # @param [Node] node
16
+ scope :select_departures, ->(node) { where(departure_id: node.id, departure_type: node.class.name) }
17
+
18
+ # returns Edge records where departure or destination are the record passed in.
19
+ # @param [Node] node
20
+ scope :select_connections, ->(node) { select_destinations(node).or(select_departures(node)) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsGraphDiagram # :nodoc:
4
+ ##
5
+ # This module represents a array of node.
6
+ class Nodes < Array
7
+ # @param [Proc] functional
8
+ # @param [Proc] meta
9
+ # @param [Array] values
10
+ # @param [Symbol] operator
11
+ # @return Proc
12
+ def read_tree(functional, meta, values, operator)
13
+ return values if !defined?(empty?) || empty?
14
+
15
+ meta[first.destination.read_tree(functional, meta, values, operator),
16
+ ActsAsGraphDiagram::Nodes.new(tail)
17
+ .read_tree(functional, meta, values, operator)]
18
+ end
19
+
20
+ # @param [Proc] functional
21
+ # @param [Nodes] values
22
+ # @return Proc
23
+ def linear(functional, values)
24
+ return self if empty? || !first.attributes.include?(:destination)
25
+
26
+ functional[first.destination,
27
+ ActsAsGraphDiagram::Nodes.new(tail)
28
+ .linear(functional, values)]
29
+ end
30
+
31
+ # @return Array
32
+ def tail
33
+ drop 1
34
+ end
35
+
36
+ # @return Proc
37
+ def confluence
38
+ # @param [Proc] functional
39
+ # @param [Nodes] values
40
+ lambda do |x, list = self|
41
+ ActsAsGraphDiagram::Nodes.new([x] + list)
42
+ end
43
+ end
44
+
45
+ # @return Proc
46
+ def append
47
+ # @param [Proc] functional
48
+ # @param [Nodes] values
49
+ lambda do |nodes = self, list|
50
+ nodes.linear confluence,
51
+ ActsAsGraphDiagram::Nodes.new(list)
52
+ end
53
+ end
54
+ end
55
+
56
+ module Node # :nodoc:
57
+ ##
58
+ # This module represents a calculation of graph.
59
+ module GraphCalculator
60
+ extend ActiveSupport::Concern
61
+
62
+ included do
63
+ # @param [Proc] functional
64
+ # @param [Proc] meta
65
+ # @param [Any] value
66
+ # @param [Symbol] operator
67
+ # @return Proc
68
+ def read_tree(functional, meta, value, operator = :self)
69
+ argument = if operator == :self
70
+ self
71
+ else
72
+ public_send(operator)
73
+ end
74
+ functional[argument,
75
+ ActsAsGraphDiagram::Nodes
76
+ .new(aheads.where.not(destination_id: id).to_a)
77
+ .read_tree(functional, meta, value, operator)]
78
+ end
79
+
80
+ # @return Integer
81
+ def sum_tree_cost
82
+ read_tree addition, addition, 0, :sum_cost
83
+ end
84
+
85
+ # @return Proc
86
+ def addition
87
+ # @param [Any] x
88
+ # @param [Any] y
89
+ ->(x, y) { x + y }
90
+ end
91
+
92
+ def sum_cost
93
+ aheads.sum(:cost)
94
+ end
95
+
96
+ # @param [Proc] functional
97
+ # @param [Proc] meta
98
+ # @return Proc
99
+ def linear(functional, meta)
100
+ raise NotImplementedError
101
+ ->(x, y) { functional[meta[x], y] }
102
+ end
103
+
104
+ # @return Proc
105
+ def append
106
+ # @param [Nodes] nodes
107
+ # @param [Array] values
108
+ lambda do |nodes = self, values|
109
+ nodes.linear confluence, ActsAsGraphDiagram::Nodes.new(values)
110
+ end
111
+ end
112
+
113
+ # @return Proc
114
+ def confluence
115
+ # @param [Node] node
116
+ # @param [Nodes] nodes
117
+ lambda do |node, nodes = self|
118
+ ActsAsGraphDiagram::Nodes.new([node] + nodes)
119
+ end
120
+ end
121
+
122
+ # @return [Node]
123
+ def assemble_tree_nodes
124
+ read_tree confluence, append, []
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'acts_as_graph_diagram/node/graph_calculator'
4
+
5
+ module ActsAsGraphDiagram # :nodoc:
6
+ module Node
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods # :nodoc:
12
+ def acts_as_node
13
+ has_many :behinds, as: :destination,
14
+ class_name: 'Edge',
15
+ dependent: :destroy
16
+ has_many :aheads, as: :departure,
17
+ class_name: 'Edge',
18
+ dependent: :destroy
19
+ include ActsAsGraphDiagram::Node::InstanceMethods
20
+ include ActsAsGraphDiagram::Node::GraphCalculator
21
+ end
22
+ end
23
+
24
+ module InstanceMethods # :nodoc:
25
+ # rubocop:disable Style/HashSyntax
26
+
27
+ # Creates a new destination record for this instance to connect the passed object.
28
+ # @param [Node] node
29
+ # @param [String] comment
30
+ # @param [Integer] cost
31
+ # @return [Edge]
32
+ def add_destination(node, comment: '', cost: 0)
33
+ aheads.select_destinations(node)
34
+ .where(comment: comment, cost: cost)
35
+ .first_or_create!
36
+ end
37
+
38
+ # Creates a new departure record for this instance to connect the passed object.
39
+ # @param [Node] node
40
+ # @param [String] comment
41
+ # @param [Integer] cost
42
+ # @return [Edge]
43
+ def add_departure(node, comment: '', cost: 0)
44
+ behinds.select_departures(node)
45
+ .where(comment: comment, cost: cost)
46
+ .first_or_create!
47
+ end
48
+
49
+ # Creates a new undirected connection record for this instance to connect the passed object.
50
+ # @param [Node] node
51
+ # @param [Boolean] directed
52
+ # @param [String] comment
53
+ # @param [Integer] cost
54
+ # @return [Edge]
55
+ def add_connection(node, directed: false, comment: '', cost: 0)
56
+ Edge.where(destination: node,
57
+ directed: directed,
58
+ departure: self,
59
+ comment: comment,
60
+ cost: cost).first_or_create!
61
+ end
62
+ # rubocop:enable Style/HashSyntax
63
+
64
+ # Returns a destination node record for the current instance.
65
+ # @param [Node] node
66
+ # @return [Edge]
67
+ def get_destination(node)
68
+ aheads.select_destinations(node).first
69
+ end
70
+
71
+ # Deletes the destination record if it exists.
72
+ # @param [Node] node
73
+ # @return [Edge|nil]
74
+ def remove_destination(node)
75
+ get_destination(node).try(:destroy)
76
+ end
77
+
78
+ # Returns a departure node record for the current instance.
79
+ # @param [Node] node
80
+ # @return [Edge]
81
+ def get_departure(node)
82
+ behinds.select_departures(node).first
83
+ end
84
+
85
+ # Deletes the destination record if it exists.
86
+ # @param [Node] node
87
+ # @return [Edge|nil]
88
+ def remove_departure(node)
89
+ get_departure(node).try(:destroy)
90
+ end
91
+
92
+ # Returns a undirected node record for the current instance.
93
+ # @param [Node] node
94
+ # @return [Edge]
95
+ def get_connection(node)
96
+ Edge.select_connections(node).first
97
+ end
98
+
99
+ # Deletes the undirected record if it exists.
100
+ # @param [Node] node
101
+ # @return [Edge|nil]
102
+ def remove_connection(node)
103
+ get_connection(node).try(:destroy)
104
+ end
105
+
106
+ # Returns true if this instance is connecting the object passed as an argument.
107
+ # @param [Node] node
108
+ # @return [Boolean]
109
+ def connecting?(node)
110
+ node.connecting_count.positive?
111
+ end
112
+
113
+ # Returns the number of objects this instance is following.
114
+ # @return [Integer]
115
+ def connecting_count
116
+ Edge.select_connections(self).count
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsGraphDiagram # :nodoc:
4
+ class Railtie < ::Rails::Railtie # :nodoc:
5
+ initializer 'acts_as_graph_diagram.active_record' do |_app|
6
+ ActiveSupport.on_load :active_record do
7
+ include ActsAsGraphDiagram::Node
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsGraphDiagram
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'acts_as_graph_diagram/version'
4
+ require 'acts_as_graph_diagram/railtie'
5
+
6
+ # Specify this extension if you want to model a graph
7
+ # network structure by providing any edge association.
8
+ module ActsAsGraphDiagram
9
+ autoload :Node, 'acts_as_graph_diagram/node'
10
+ autoload :EdgeScopes, 'acts_as_graph_diagram/edge_scopes.rb'
11
+ require 'acts_as_graph_diagram/railtie' if defined?(Rails) && Rails::VERSION::MAJOR >= 3
12
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ rails generate acts_as_graph_diagram
3
+
4
+ no need to specify a name after acts_as_graph_diagram as you can not change the model name from Follow
5
+ the acts_as_graph_diagram_migration file will be created in db/migrate
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ class ActsAsGraphDiagramGenerator < Rails::Generators::Base # :nodoc:
7
+ include Rails::Generators::Migration
8
+
9
+ def self.source_root
10
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
11
+ end
12
+
13
+ # Implement the required interface for Rails::Generators::Migration.
14
+ # taken from https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record.rb
15
+ def self.next_migration_number(dirname)
16
+ if ActiveRecord::Base.timestamped_migrations
17
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
18
+ else
19
+ format('%.3d', (current_migration_number(dirname) + 1))
20
+ end
21
+ end
22
+
23
+ def create_migration_file
24
+ migration_template 'migration.rb', 'db/migrate/acts_as_graph_diagram_migration.rb'
25
+ end
26
+
27
+ def create_model
28
+ template 'model.rb', File.join('app/models', 'edge.rb')
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActsAsGraphDiagramMigration < ActiveRecord::Migration[4.2] # :nodoc:
4
+ def self.up
5
+ create_table :edges, force: true do |t|
6
+ t.string :comment, default: ''
7
+ t.integer :cost, default: 0
8
+ t.boolean :directed, default: true
9
+ t.references :destination, polymorphic: true, null: true
10
+ t.references :departure, polymorphic: true, null: true
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :edges, %w[departure_id departure_type], name: 'fk_edges_departure'
15
+ add_index :edges, %w[destination_id destination_type], name: 'fk_edges_destination'
16
+ end
17
+
18
+ def self.down
19
+ drop_table :edges
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: edges
6
+ #
7
+ # id :integer not null, primary key
8
+ # comment :string default("")
9
+ # cost :integer default(0)
10
+ # directed :boolean default(TRUE)
11
+ # destination_type :string
12
+ # destination_id :integer
13
+ # departure_type :string
14
+ # departure_id :integer
15
+ # created_at :datetime
16
+ # updated_at :datetime
17
+ #
18
+ class Edge < ActiveRecord::Base
19
+ extend ActsAsGraphDiagram::Node
20
+ include ActsAsGraphDiagram::EdgeScopes
21
+
22
+ belongs_to :destination, polymorphic: true, optional: true
23
+ belongs_to :departure, polymorphic: true, optional: true
24
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class ActsAsGraphDiagramTest < ActiveSupport::TestCase
6
+ test 'it has a version number' do
7
+ assert ActsAsGraphDiagram::VERSION
8
+ end
9
+
10
+ test 'be defined' do
11
+ assert God.first.respond_to?(:aheads)
12
+ assert God.first.respond_to?(:behinds)
13
+ assert God.first.respond_to?(:add_destination)
14
+ assert God.first.respond_to?(:add_departure)
15
+ assert God.first.respond_to?(:get_destination)
16
+ assert God.first.respond_to?(:get_departure)
17
+ assert God.first.respond_to?(:remove_destination)
18
+ assert God.first.respond_to?(:remove_departure)
19
+ assert God.first.respond_to?(:connecting?)
20
+ assert God.first.respond_to?(:connecting_count)
21
+ assert God.first.respond_to?(:add_connection)
22
+ assert God.first.respond_to?(:sum_cost)
23
+ assert God.first.respond_to?(:sum_tree_cost)
24
+ assert God.first.respond_to?(:assemble_tree_nodes)
25
+ end
26
+
27
+ test 'calculate sum_cost' do
28
+ God.find(3).add_destination(God.find(5), cost: 4)
29
+ assert_equal God.find(3).sum_cost, 4
30
+ end
31
+
32
+ test 'calculate sum_tree_cost' do
33
+ God.find(4).add_destination(God.find(6), cost: 4)
34
+ God.find(6).add_destination(God.find(7), cost: 3)
35
+ assert_equal God.find(4).sum_tree_cost, 7
36
+ end
37
+
38
+ test 'call assemble_tree_nodes' do
39
+ God.find(4).add_destination(God.find(6), cost: 4)
40
+ God.find(6).add_destination(God.find(7), cost: 3)
41
+ God.find(6).add_destination(God.find(7), cost: 3)
42
+ assert_equal God.find(4).assemble_tree_nodes.size, 3
43
+ end
44
+ end