society 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Society
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.0"
3
3
  end
data/society.gemspec CHANGED
@@ -6,11 +6,11 @@ require 'society/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "society"
8
8
  spec.version = Society::VERSION
9
- spec.authors = ["Coraline Ada Ehmke", "Kerri Miller"]
10
- spec.email = ["coraline@instructure.com", "kerrizor@gmail.com"]
9
+ spec.authors = ["Coraline Ada Ehmke"]
10
+ spec.email = ["coraline@instructure.com"]
11
11
  spec.summary = %q{Social graph for Ruby objects}
12
- spec.description = %q{Social graph for Ruby objects}
13
- spec.homepage = "https://github.com/Bantik/society"
12
+ spec.description = %q{Social graph for Ruby objects. Based on an original idea by Kerri Miller.}
13
+ spec.homepage = "https://github.com/CoralineAda/society"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -26,7 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency "thor"
27
27
 
28
28
  spec.add_development_dependency "bundler", "~> 1.6"
29
- spec.add_development_dependency "pry"
30
29
  spec.add_development_dependency "rake"
31
30
  spec.add_development_dependency "rspec"
32
31
  spec.add_development_dependency "simplecov"
data/spec/cli_spec.rb CHANGED
@@ -5,7 +5,7 @@ describe Society::CLI do
5
5
 
6
6
  describe "#from" do
7
7
 
8
- let(:parser) { Society::Parser.new(nil) }
8
+ let(:parser) { Society::Parser.new([]) }
9
9
 
10
10
  it "invokes Society with a path" do
11
11
  allow(parser).to receive(:report)
@@ -1,30 +1,224 @@
1
1
  require 'spec_helper'
2
2
  require 'analyst'
3
3
 
4
- describe Society::ObjectGraph do
4
+ describe Society::Node do
5
+
6
+ let(:edge_a) { Society::Edge.new(to: "A", weight: 1) }
7
+ let(:edge_b) { Society::Edge.new(to: "B", weight: 1) }
8
+
9
+ describe "#initialize" do
10
+
11
+ it "returns a node object" do
12
+ node = Society::Node.new(name: "A", type: :class)
13
+ expect(node.class.name).to eq("Society::Node")
14
+ end
15
+
16
+ it "initializes its edges" do
17
+ node = Society::Node.new(name: "A", type: :class, edges: [edge_a, edge_b])
18
+ expect(node.edges.first.to).to eq("A")
19
+ expect(node.edges.first.weight).to eq(1)
20
+ end
21
+
22
+ it "initializes metainformation" do
23
+ node = Society::Node.new(name: "A", type: :class, meta: [true])
24
+ expect(node.meta).to match_array [true]
25
+ end
26
+
27
+ end
28
+
29
+ describe "#intersects?" do
30
+ let(:node) { Society::Node.new(name: "A", type: :class) }
31
+ let(:nonintersecting_a) { Society::Node.new(name: "A", type: :module) }
32
+ let(:nonintersecting_b) { Society::Node.new(name: "B", type: :class) }
33
+ let(:intersecting) { Society::Node.new(name: "A", type: :class) }
34
+
35
+ it "detects intersections iff name and type are the same" do
36
+ expect(node.intersects?(nonintersecting_a)).to eq(false)
37
+ expect(node.intersects?(nonintersecting_b)).to eq(false)
38
+ expect(node.intersects?(intersecting)).to eq(true)
39
+ end
40
+
41
+ end
42
+
43
+ describe "#+" do
44
+ let(:node_a) { Society::Node.new(name: "A", type: :class, edges: [edge_a]) }
45
+ let(:node_b) { Society::Node.new(name: "A", type: :class, edges: [edge_b]) }
46
+ let(:node_c) { Society::Node.new(name: "A", type: :class, unresolved: [edge_b]) }
47
+
48
+ it "allows addition of intersecting nodes, accumulating edges" do
49
+ node_d = node_a + node_b
50
+ expect(node_d.edges.sort_by(&:to)).to eq([edge_a, edge_b])
51
+
52
+ node_e = node_a + node_c
53
+ expect(node_e.edges).to eq([edge_a])
54
+ expect(node_e.unresolved).to eq([edge_b])
55
+ end
56
+
57
+ it "accumulates edges upon addition" do
58
+ node_d = node_b + node_a + node_b
59
+ expect(node_d.edges.map(&:to).sort).to match_array ["A", "B"]
60
+ expect(node_d.edges.sort_by(&:to).map(&:weight)).to match_array [1, 2]
61
+
62
+ node_e = node_a + node_b + node_a
63
+ expect(node_e.edges.map(&:to).sort).to match_array ["A", "B"]
64
+ expect(node_e.edges.sort_by(&:to).map(&:weight)).to match_array [2, 1]
65
+ end
66
+
67
+ it "treats addition against itself as a no-op" do
68
+ node_d = node_a + node_a
69
+ expect(node_d.object_id).to eq(node_a.object_id)
70
+ end
71
+
72
+ end
73
+
74
+ describe "#to_s" do
75
+ let(:node_a) { Society::Node.new(name: "A", type: :class, edges: [edge_a]) }
76
+
77
+ it "returns the node's name as a string" do
78
+ expect(node_a.to_s).to eq("A")
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ describe Society::Edge do
85
+
86
+ describe "#initialize" do
87
+ it "returns an edge object" do
88
+ expect(Society::Edge.new(to: "A").class.name).to eq("Society::Edge")
89
+ end
90
+
91
+ it "uses a default weight of 1 for an edge" do
92
+ expect(Society::Edge.new(to: "A").weight).to eq(1)
93
+ end
94
+
95
+ it "allows a weight to be specified" do
96
+ expect(Society::Edge.new(to: "A", weight: 3).weight).to eq(3)
97
+ end
98
+ end
99
+
100
+ describe "#+" do
101
+ let(:edge_a1) { Society::Edge.new(to: "A", weight: 1) }
102
+ let(:edge_a2) { Society::Edge.new(to: "A", weight: 2) }
103
+ let(:edge_b) { Society::Edge.new(to: "B") }
104
+
105
+ it "adds the weights of two like edges" do
106
+ edge_a3 = edge_a1 + edge_a2
107
+ expect(edge_a3.weight).to eq(3)
108
+ end
109
+
110
+ it "returns nil if the edge targets are not aligned" do
111
+ edge_z = edge_a1 + edge_b
112
+ expect(edge_z).to eq(nil)
113
+ end
5
114
 
6
- let(:graph) { Struct.new(:nodes, :edges)}
7
- let(:node) { Struct.new(:full_name) }
8
- let(:edge) { Struct.new(:from, :to) }
115
+ end
116
+
117
+ describe "#+" do
118
+ let(:edge) { Society::Edge.new(to: "A", weight: 1) }
119
+
120
+ it "returns the name of the target node as a string" do
121
+ expect(edge.to_s).to eq("A")
122
+ end
123
+
124
+ end
9
125
 
10
- let(:node_instance_1) { node.new("sample_node")}
11
- let(:node_instance_2) { node.new("other_node")}
12
- let(:edge_instance) { edge.new(node_instance_1, node_instance_2) }
126
+ end
13
127
 
14
- let(:graph) { Society::ObjectGraph.new(
15
- nodes: [node_instance_1, node_instance_2],
16
- edges: [edge_instance]
17
- )
18
- }
128
+ describe Society::ObjectGraph do
129
+
130
+ let(:edge_a) { Society::Edge.new(to: "A") }
131
+ let(:edge_b) { Society::Edge.new(to: "B") }
132
+ let(:edge_d) { Society::Edge.new(to: "D") }
133
+
134
+ let(:node_a) { Society::Node.new(name: "A", type: :class, edges: [edge_b]) }
135
+ let(:node_b) { Society::Node.new(name: "B", type: :class, edges: [edge_d]) }
136
+ let(:node_c) { Society::Node.new(name: "C", type: :class, edges: [edge_d]) }
137
+ let(:node_d) { Society::Node.new(name: "D", type: :class, edges: [edge_a, edge_b]) }
19
138
 
20
139
  describe "#initialize" do
21
- it "initializes its nodes" do
22
- expect(graph.nodes.first.full_name).to eq "sample_node"
140
+
141
+ it "returns an ObjectGraph object" do
142
+ expect(Society::ObjectGraph.new.class.name).to eq("Society::ObjectGraph")
143
+ end
144
+
145
+ it "is empty by default" do
146
+ expect(Society::ObjectGraph.new).to eq([])
23
147
  end
24
- it "initializes its edges" do
25
- expect(graph.edges.first.to.full_name).to eq "other_node"
148
+
149
+ it "allows several nodes to be given" do
150
+ expect(Society::ObjectGraph.new(node_a, node_b).first).to eq(node_a)
151
+ end
152
+
153
+ it "allows a list of nodes to be given" do
154
+ expect(Society::ObjectGraph.new([node_a, node_b]).first).to eq(node_a)
155
+ end
156
+
157
+ end
158
+
159
+ describe "#+" do
160
+
161
+ let(:node_a2) { Society::Node.new(name: "A", type: :class, edges: [edge_b, edge_d]) }
162
+
163
+ let(:graph_a) { Society::ObjectGraph.new([node_a]) }
164
+ let(:graph_b) { Society::ObjectGraph.new([node_b]) }
165
+ let(:graph_c) { Society::ObjectGraph.new([node_a2, node_c, node_d]) }
166
+
167
+ it "treats addition of an empty graph as a no-op" do
168
+ expect(graph_a + Society::ObjectGraph.new).to eq(graph_a)
169
+ end
170
+
171
+ it "does not modify nodes when none intersect" do
172
+ expect(graph_a + graph_b).to eq([node_a, node_b])
173
+ end
174
+
175
+ it "sums nodes which intersect" do
176
+ graph_d = graph_a + graph_c
177
+ node = graph_d.select { |node| node.name == "A" }.first
178
+
179
+ expect(graph_d.length).to eq(3)
180
+ expect(node.edges.select { |n| n.to == "B" }.first.weight ).to eq(2)
181
+ end
182
+
183
+ end
184
+
185
+ describe "#<<" do
186
+
187
+ let(:node_a2) { Society::Node.new(name: "A", type: :class, edges: [edge_b, edge_d]) }
188
+ let(:graph_a) { Society::ObjectGraph.new([node_a]) }
189
+
190
+ it "returns a new graph object without mutating the original" do
191
+ graph_b = graph_a << node_b
192
+ expect(graph_a.length).to eq(1)
193
+ expect(graph_b.object_id).not_to eq(graph_a.object_id)
194
+ end
195
+
196
+ it "sums nodes which intersect" do
197
+ graph_b = graph_a << node_a2
198
+ node = graph_b.select { |node| node.name == "A" }.first
199
+ expect(node.edges.select { |n| n.to == "B" }.first.weight ).to eq(2)
200
+ end
201
+
202
+ end
203
+
204
+ describe "#to_h" do
205
+
206
+ let(:graph_a) { Society::ObjectGraph.new([node_a]) }
207
+
208
+ it "returns a hash representing the graph" do
209
+ expect(graph_a.to_h).to eq({node_a.name => node_a.edges})
210
+ end
211
+
212
+ end
213
+
214
+ describe "#to_json" do
215
+
216
+ let(:graph_a) { Society::ObjectGraph.new([node_a]) }
217
+
218
+ it "returns a json string representing the graph" do
219
+ expect(graph_a.to_json).to eq('{"A":{"B":1}}')
26
220
  end
27
221
 
28
222
  end
29
223
 
30
- end
224
+ end
data/spec/parser_spec.rb CHANGED
@@ -1,36 +1,227 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Society::Parser do
4
+ let(:source) { "class Ship; end" }
5
+ let(:nested_source) { "module Ship; class Anchor; end; end;" }
6
+ let(:inherited_source) { "class Carrier < Ship; end" }
7
+ let(:namespaced_source) { "class Ship::Anchor; end" }
4
8
 
5
- describe "::for_files" do
9
+ describe "::for_source" do
10
+
11
+ context "listing nodes" do
12
+ it "returns a parser object" do
13
+ expect(Society::Parser.for_source(source).class.name).to eq("Society::Parser")
14
+ end
15
+
16
+ it "will accept multiple source strings" do
17
+ expect(Society::Parser.for_source(source, nested_source).class.name).to eq("Society::Parser")
18
+ end
19
+
20
+ it "parses simple source trees" do
21
+ expect(Society::Parser.for_source(source).classes).to eq(["Ship"])
22
+ end
23
+
24
+ it "parses source trees with inheritance" do
25
+ expect(Society::Parser.for_source(inherited_source).classes).to eq(["Carrier"])
26
+ end
27
+
28
+ it "parses namespaced nodes" do
29
+ expect(Society::Parser.for_source(nested_source).classes).to eq(["Ship", "Ship::Anchor"])
30
+ expect(Society::Parser.for_source(namespaced_source).classes).to eq(["Ship::Anchor"])
31
+ end
32
+
33
+ it "tracks if a node is a class or module" do
34
+ parser = Society::Parser.for_source(nested_source)
35
+ expect(parser.graph.select { |node| node.name == "Ship" }.first.type).to eq(:module)
36
+ expect(parser.graph.select { |node| node.name == "Ship::Anchor" }.first.type).to eq(:class)
37
+ end
6
38
 
7
- it "returns a parser object" do
8
- expect(Society::Parser.for_files('./spec/fixtures/for_parser_spec').class.name).to eq("Society::Parser")
9
39
  end
10
40
 
11
- it "initializes the parser with an Analyzer object" do
12
- expect(Society::Parser.for_files('./spec/fixtures/for_parser_spec').analyzer.class.name).to eq("Analyst::Parser")
41
+ context "detecting edges" do
42
+ let(:source) { "class Ship; def initialize; @engine = Engine.new; end; end; class Engine; end;" }
43
+ let(:namespaced_source) { "class Ship; def initialize; @engine = Engine::Diesel.new; end; end; class Engine::Diesel; end" }
44
+ let(:unknown_edge_source) { "class Ship; def initialize; @engine = Engine::UFO.new; end; end" }
45
+
46
+ it "detects simple edges for classes" do
47
+ graph = Society::Parser.for_source(source).graph
48
+ edge = graph.first.edges.first
49
+ expect(edge.class.name).to eq("Society::Edge")
50
+ expect(graph.first.edges.map(&:to)).to eq(["Engine"])
51
+ end
52
+
53
+ it "detects simple edges for namespaced classes" do
54
+ expect(Society::Parser.for_source(namespaced_source).graph.first.edges.map(&:to)).to eq(["Engine::Diesel"])
55
+ end
56
+
57
+ it "rejects edges which point to nodes of which it is unaware" do
58
+ expect(Society::Parser.for_source(unknown_edge_source).graph.first.edges.empty?).to eq(true)
59
+ end
13
60
  end
14
61
 
15
- end
62
+ context "detecting activerecord edges from belongs_to, has_many and through" do
63
+ let(:source) {
64
+ <<-CODE
65
+ class Assembly < ActiveRecord::Base
66
+ has_many :manifests
67
+ has_many :parts, through: :manifests
68
+ end
69
+
70
+ class Manifest < ActiveRecord::Base
71
+ belongs_to :assembly
72
+ belongs_to :part
73
+ end
74
+
75
+ class Part < ActiveRecord::Base
76
+ has_many :manifests
77
+ has_many :assemblies, through: :manifests
78
+ end
79
+ CODE
80
+ }
81
+
82
+ let(:parser) { Society::Parser.for_source(source) }
83
+ let(:assembly) { parser.graph.detect {|c| c.name == "Assembly" } }
84
+ let(:manifest) { parser.graph.detect {|c| c.name == "Manifest" } }
85
+ let(:part) { parser.graph.detect {|c| c.name == "Part" } }
86
+
87
+ it "records `has_many` and `has_many through:` associations" do
88
+ expect(assembly.edges.map(&:to).sort).to match_array %w(Manifest Part)
89
+ expect(part.edges.map(&:to).sort).to match_array %w(Assembly Manifest)
90
+ end
16
91
 
17
- describe "::for_source" do
92
+ it "records `belongs_to` associations" do
93
+ expect(manifest.edges.map(&:to).sort).to match_array %w(Assembly Part)
94
+ end
95
+ end
18
96
 
19
- let(:source) { "class Ship; end" }
97
+ context "detecting activerecord edges with `class_name` specified" do
98
+ let(:source) {<<-CODE
99
+ class Post < ActiveRecord::Base
100
+ belongs_to :author, :class_name => "User"
101
+ belongs_to :editor, :class_name => "User"
102
+ end
103
+
104
+ class User < ActiveRecord::Base
105
+ has_many :authored_posts, :foreign_key => "author_id", :class_name => "Post"
106
+ has_many :edited_posts, :foreign_key => "editor_id", :class_name => "Post"
107
+ end
108
+ CODE
109
+ }
110
+
111
+ let(:parser) { Society::Parser.for_source(source) }
112
+ let(:user) { parser.graph.detect {|c| c.name == "User" } }
113
+ let(:post) { parser.graph.detect {|c| c.name == "Post" } }
114
+
115
+ it "records belongs_to's" do
116
+ expect(post.edges.map(&:to)).to match_array %w(User)
117
+ expect(post.edges.map(&:weight)).to match_array [2]
118
+ end
119
+
120
+ it "records has_many's" do
121
+ expect(user.edges.map(&:to)).to match_array %w(Post)
122
+ expect(user.edges.map(&:weight)).to match_array [2]
123
+ end
124
+ end
125
+
126
+ context "detecting activerecord edges with `class_name` or `source` specified" do
127
+ let(:source) { <<-CODE
128
+ class Post < ActiveRecord::Base
129
+ has_many :post_authorings, :foreign_key => :authored_post_id
130
+ has_many :authors, :through => :post_authorings, :source => :post_author
131
+ belongs_to :editor, :class_name => "User"
132
+ end
133
+
134
+ class User < ActiveRecord::Base
135
+ has_many :post_authorings, :foreign_key => :post_author_id
136
+ has_many :authored_posts, :through => :post_authorings
137
+ has_many :edited_posts, :foreign_key => :editor_id, :class_name => "Post"
138
+ end
139
+
140
+ class PostAuthoring < ActiveRecord::Base
141
+ belongs_to :post_author, :class_name => "User"
142
+ belongs_to :authored_post, :class_name => "Post"
143
+ end
144
+ CODE
145
+ }
146
+
147
+ let(:parser) { Society::Parser.for_source(source) }
148
+ let(:user) { parser.graph.detect {|c| c.name == "User" } }
149
+ let(:post) { parser.graph.detect {|c| c.name == "Post" } }
150
+ let(:post_authoring) { parser.graph.detect {|c| c.name == "PostAuthoring" } }
151
+
152
+ it "handles `source` and `class_name` correctly" do
153
+ expect(post.edges.map(&:to).sort).to match_array %w(PostAuthoring User)
154
+ expect(post.edges.sort_by(&:to).map(&:weight)).to match_array [1, 2]
155
+ end
156
+
157
+ it "handles `through` and `class_name` correctly" do
158
+ expect(user.edges.map(&:to).sort).to match_array %w(PostAuthoring Post)
159
+ expect(user.edges.sort_by(&:to).map(&:weight)).to match_array [1, 2]
160
+ end
161
+
162
+ it "handles `class_name` correctly" do
163
+ expect(post_authoring.edges.map(&:to).sort).to match_array %w(Post User)
164
+ end
165
+ end
166
+
167
+ context "detecting activerecord edges with polymorphic associations" do
168
+ let(:source) { <<-CODE
169
+ class Picture < ActiveRecord::Base
170
+ belongs_to :imageable, polymorphic: true
171
+ end
172
+
173
+ class Employee < ActiveRecord::Base
174
+ has_many :pictures, as: :imageable
175
+ end
176
+
177
+ class Product < ActiveRecord::Base
178
+ has_many :pictures, as: :imageable
179
+ end
180
+ CODE
181
+ }
182
+
183
+ let(:parser) { Society::Parser.for_source(source) }
184
+ let(:picture) { parser.graph.detect {|c| c.name == "Picture" } }
185
+ let(:employee) { parser.graph.detect {|c| c.name == "Employee" } }
186
+ let(:product) { parser.graph.detect {|c| c.name == "Product" } }
187
+
188
+ it "records the `polymorphic: true` side of the association" do
189
+ expect(picture.edges.map(&:to).sort).to match_array %w(Employee Product)
190
+ end
191
+
192
+ it "records the `as: :something` side of the asociation" do
193
+ expect(employee.edges.map(&:to).sort).to match_array %w(Picture)
194
+ expect(product.edges.map(&:to).sort).to match_array %w(Picture)
195
+ end
196
+
197
+ end
198
+
199
+ end
200
+
201
+ describe "::for_files" do
20
202
 
21
203
  it "returns a parser object" do
22
- expect(Society::Parser.for_source(source).class.name).to eq("Society::Parser")
204
+ expect(Society::Parser.for_files('./spec/fixtures/for_parser_spec').class.name).to eq("Society::Parser")
205
+ expect(Society::Parser.for_files('./spec/fixtures/for_parser_spec').classes).to eq(["Whaler"])
23
206
  end
24
207
 
25
- it "initializes the parser with an Analyzer object" do
26
- expect(Society::Parser.for_source(source).analyzer.class.name).to eq("Analyst::Parser")
208
+ end
209
+
210
+ describe "#initialize" do
211
+ it "returns a parser object" do
212
+ expect(Society::Parser.new([]).class.name).to eq("Society::Parser")
27
213
  end
214
+ end
28
215
 
216
+ describe "#graph" do
217
+ it "returns a graph object" do
218
+ expect(Society::Parser.new([]).graph.class.name).to eq("Society::ObjectGraph")
219
+ end
29
220
  end
30
221
 
31
222
  describe "#report" do
32
223
 
33
- let(:parser) { Society::Parser.new(nil) }
224
+ let(:parser) { Society::Parser.new([]) }
34
225
  let(:formatter) { double.as_null_object }
35
226
 
36
227
  before do