society 0.13.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.bowerrc +3 -0
  3. data/.gitignore +2 -14
  4. data/README.md +18 -17
  5. data/bower.json +6 -0
  6. data/lib/society.rb +16 -6
  7. data/lib/society/association_processor.rb +206 -0
  8. data/lib/society/cli.rb +12 -9
  9. data/lib/society/edge.rb +15 -0
  10. data/lib/society/formatter/graph/json.rb +44 -0
  11. data/lib/society/formatter/report/html.rb +73 -0
  12. data/lib/society/formatter/report/json.rb +39 -0
  13. data/lib/society/formatter/report/templates/components/.gitignore +7 -0
  14. data/lib/society/formatter/report/templates/components/d3/d3.min.js +5 -0
  15. data/lib/society/formatter/report/templates/components/society-assets/society.css +70 -0
  16. data/lib/society/formatter/report/templates/components/society-assets/society.js +420 -0
  17. data/lib/society/formatter/report/templates/index.htm.haml +48 -0
  18. data/lib/society/object_graph.rb +3 -2
  19. data/lib/society/parser.rb +49 -32
  20. data/lib/society/reference_processor.rb +60 -0
  21. data/lib/society/version.rb +1 -1
  22. data/society.gemspec +4 -1
  23. data/society_graph.json +1 -0
  24. data/spec/association_processor_spec.rb +174 -0
  25. data/spec/cli_spec.rb +25 -0
  26. data/spec/fixtures/foo.rb +6 -18
  27. data/spec/formatter/graph/json_spec.rb +52 -0
  28. data/spec/formatter/report/html_spec.rb +75 -0
  29. data/spec/formatter/report/json_spec.rb +70 -0
  30. data/spec/object_graph_spec.rb +30 -0
  31. data/spec/parser_spec.rb +61 -0
  32. data/spec/reference_processor_spec.rb +18 -0
  33. data/spec/spec_helper.rb +3 -0
  34. metadata +77 -13
  35. data/lib/society/matrix.rb +0 -36
  36. data/lib/society/node.rb +0 -19
  37. data/spec/fixtures/bar.rb +0 -6
  38. data/spec/matrix_spec.rb +0 -27
  39. data/spec/node_spec.rb +0 -30
@@ -0,0 +1,48 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %meta{:charset => "utf-8"}
5
+ %title Society Relational Analysis
6
+ :css
7
+ @import url(stylesheets/society.css);
8
+ %script{:src => "javascripts/d3.min.js"}
9
+ %script{:src => "javascripts/society.js"}
10
+ :css
11
+ body {
12
+ background-color: #fff;
13
+ padding: 1em;
14
+ font-family: Consolas, Verdana, sans-serif;
15
+ font-size: 12px;
16
+ }
17
+ %body
18
+ %div{:style => "text-align: center;"}
19
+ %h1 Relational Matrices
20
+ %h3
21
+ Generated by
22
+ %a{:href => "https://github.com/Bantik/society"} Society
23
+ %hr{:size => "1"}/
24
+ %div{:style => "text-align: center;"}
25
+ %h2 Heatmap
26
+ #heatmap
27
+ %hr{:size => "1"}/
28
+ %div{:style => "text-align: center;"}
29
+ %h2 Network
30
+ %p
31
+ %em
32
+ Hover over a class name to see incoming references (green) and outgoing
33
+ references (red).
34
+ #network{:style => "height: 100%; width: 100%;"}
35
+ :javascript
36
+ var data = JSON.parse('#{json_data}');
37
+ Society.generate("#network", {
38
+ data: {
39
+ json: data
40
+ }
41
+ });
42
+ Society.generate("#heatmap", {
43
+ type: "heatmap",
44
+ data: {
45
+ json: data
46
+ }
47
+ });
48
+
@@ -2,10 +2,11 @@ module Society
2
2
 
3
3
  class ObjectGraph
4
4
 
5
- attr_accessor :nodes
5
+ attr_accessor :nodes, :edges
6
6
 
7
- def initialize(nodes: nodes=[])
7
+ def initialize(nodes: nodes=[], edges: edges=[])
8
8
  @nodes = nodes
9
+ @edges = edges
9
10
  end
10
11
 
11
12
  end
@@ -1,52 +1,69 @@
1
- require 'fukuzatsu'
2
-
3
1
  module Society
4
2
 
5
- class Parser < Fukuzatsu::Parser
3
+ class Parser
4
+
5
+ def self.for_files(file_path)
6
+ new(::Analyst.for_files(file_path))
7
+ end
8
+
9
+ def self.for_source(source)
10
+ new(::Analyst.for_source(source))
11
+ end
12
+
13
+ attr_reader :analyzer
14
+
15
+ def initialize(analyzer)
16
+ @analyzer = analyzer
17
+ end
18
+
19
+ def report(format, output_path=nil)
20
+ raise ArgumentError, "Unknown format #{format}" unless known_formats.include?(format)
21
+ options = { json_data: json_data }
22
+ options[:output_path] = output_path unless output_path.nil?
23
+ FORMATTERS[format].new(options).write
24
+ end
25
+
26
+ private
6
27
 
7
- attr_reader :parsed_files
28
+ FORMATTERS = {
29
+ html: Society::Formatter::Report::HTML,
30
+ json: Society::Formatter::Report::Json
31
+ }
8
32
 
9
- def initialize(start_path)
10
- super(start_path, :none, 0)
11
- @parsed_files = parse_files
33
+ def classes
34
+ @classes ||= analyzer.classes
12
35
  end
13
36
 
14
37
  def class_graph
15
38
  @class_graph ||= begin
16
- graph = ObjectGraph.new
17
- graph.nodes = parsed_files.map do |parsed_file|
18
- Node.new(
19
- name: parsed_file.class_name,
20
- address: parsed_file.path_to_file,
21
- edges: parsed_file.class_references
22
- )
23
- end
24
- graph
39
+ associations = associations_from(classes) + references_from(classes)
40
+ # TODO: merge identical classes, and (somewhere else) deal with
41
+ # identical associations too. need a WeightedEdge, and each
42
+ # one will be unique on [from, to], but will have a weight
43
+
44
+ ObjectGraph.new(nodes: classes, edges: associations)
25
45
  end
26
46
  end
27
47
 
28
- def method_graph
29
- @method_graph ||= begin
30
- graph = ObjectGraph.new
31
- target = parsed_files.first
32
- graph.nodes = target.methods.map do |method|
33
- Node.new(
34
- name: method.name,
35
- address: target.class_name,
36
- edges: method.references
37
- )
38
- end
39
- graph
40
- end
48
+ def json_data
49
+ Society::Formatter::Graph::JSON.new(class_graph).to_json
50
+ end
51
+
52
+ def known_formats
53
+ FORMATTERS.keys
41
54
  end
42
55
 
43
- def matrix(graph)
44
- Society::Matrix.new(graph.nodes)
56
+ def associations_from(all_classes)
57
+ @association_processor ||= AssociationProcessor.new(all_classes)
58
+ @association_processor.associations
45
59
  end
46
60
 
47
- def reset_output_directory
61
+ def references_from(all_classes)
62
+ @reference_processor ||= ReferenceProcessor.new(all_classes)
63
+ @reference_processor.references
48
64
  end
49
65
 
50
66
  end
51
67
 
52
68
  end
69
+
@@ -0,0 +1,60 @@
1
+ module Society
2
+
3
+ class ReferenceProcessor
4
+
5
+ attr_reader :classes, :references, :unresolved_references
6
+
7
+ def initialize(classes)
8
+ @classes = classes
9
+ @references = []
10
+ @unresolved_references = []
11
+ process
12
+ end
13
+
14
+ private
15
+
16
+ def process
17
+ classes.each do |klass|
18
+ klass.constants.each do |const|
19
+ if assigned_constant(const, klass)
20
+ next
21
+ elsif target = perfect_match_for(const)
22
+ @references << Edge.new(from: klass,
23
+ to: target,
24
+ meta: {
25
+ type: :perfect,
26
+ entity: const})
27
+ elsif target = partial_match_for(const)
28
+ @references << Edge.new(from: klass,
29
+ to: target,
30
+ meta: {
31
+ type: :partial,
32
+ entity: const})
33
+ else
34
+ @unresolved_references << { class: klass,
35
+ target_name: const.full_name,
36
+ constant: const }
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def perfect_match_for(const)
43
+ classes.detect { |klass| klass.full_name == const.name }
44
+ end
45
+
46
+ def partial_match_for(const)
47
+ partial_matches = classes.select do |klass|
48
+ klass.full_name.include? const.full_name
49
+ end
50
+ partial_matches.first if partial_matches.size == 1
51
+ end
52
+
53
+ def assigned_constant(const, klass)
54
+ klass.constant_assignments.map(&:name).include? const.name
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
@@ -1,3 +1,3 @@
1
1
  module Society
2
- VERSION = "0.13.2"
2
+ VERSION = "1.0.0"
3
3
  end
data/society.gemspec CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "fukuzatsu", ">= 1.0.5"
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "analyst", ">= 1.0.0"
22
23
  spec.add_dependency "parser"
23
24
  spec.add_dependency "rainbow"
24
25
  spec.add_dependency "terminal-table"
@@ -29,5 +30,7 @@ Gem::Specification.new do |spec|
29
30
  spec.add_development_dependency "rake"
30
31
  spec.add_development_dependency "rspec"
31
32
  spec.add_development_dependency "simplecov"
33
+ spec.add_development_dependency "byebug"
34
+ spec.add_development_dependency "awesome_print"
32
35
 
33
36
  end
@@ -0,0 +1 @@
1
+ {"nodes":[{"name":"Account"},{"name":"Attachment"},{"name":"Attempt"},{"name":"Auth"},{"name":"Certificate"},{"name":"Collection"},{"name":"CollectionEnrollment"},{"name":"CollectionGroupEnrollment"},{"name":"CollectionItem"},{"name":"ConcreteConfig"},{"name":"CourseTemplate"},{"name":"CustomField"},{"name":"EnrollingMembership"},{"name":"Enrollment"},{"name":"Group"},{"name":"GroupEnrollment"},{"name":"ImportObject"},{"name":"LearnerCourse"},{"name":"LearnerCustomFieldValue"},{"name":"LearnerFile"},{"name":"LearnerLibraryCourse"},{"name":"LearnerLiveCourse"},{"name":"LearnerSlide"},{"name":"LiveCourse"},{"name":"LiveCourseEnrollment"},{"name":"LiveCourseEnrollmentSource"},{"name":"LiveCourseGroupEnrollment"},{"name":"LiveCourseSession"},{"name":"LiveCourseSessionRegistration"},{"name":"Manager"},{"name":"Membership"},{"name":"MembershipBuilder"},{"name":"NotifiableEnrollment"},{"name":"NotifiableLiveCourseEnrollment"},{"name":"NotifiableLiveCourseSessionRegistration"},{"name":"Notification"},{"name":"NotificationProfile"},{"name":"PasswordToken"},{"name":"Presence"},{"name":"QuestionAnswer"},{"name":"QuestionHash"},{"name":"QuestionResponse"},{"name":"RequestHost"},{"name":"Rule"},{"name":"RulesAttributesNormalizer"},{"name":"SessionToken"},{"name":"Slide"},{"name":"User"},{"name":"Account::Config"},{"name":"Attempt::NullFurthestSlide"},{"name":"Auth::Basic"},{"name":"Auth::CAS"},{"name":"GroupEnrollment::StatisticsCalculator"},{"name":"QuestionHash::Factoid"},{"name":"QuestionHash::Factoid::MultipleChoiceQuestion"},{"name":"QuestionHash::Factoid::Statement"},{"name":"QuestionHash::Factoid::TrueFalseQuestion"},{"name":"QuestionHash::Glossary"},{"name":"QuestionHash::Glossary::MultipleChoiceQuestion"},{"name":"QuestionHash::Glossary::TrueFalseQuestion"},{"name":"QuestionHash::OrderedList"},{"name":"QuestionHash::OrderedList::MultipleChoiceQuestion"},{"name":"QuestionHash::OrderedList::ReorderQuestion"},{"name":"Rule::InvalidPredicate"},{"name":"RulesAttributesNormalizer::FieldRules"},{"name":"Notifications::AuthorCourseDigest"},{"name":"Notifications::CourseAssignedNotificationBuilder"},{"name":"Notifications::CourseDigest"},{"name":"Notifications::LearnerCourseDigest"},{"name":"Notifications::LiveCourseInvitationNotificationBuilder"},{"name":"Notifications::LiveCourseNotificationBuilder"},{"name":"Notifications::LiveCourseRegisteredForSessionNotificationBuilder"},{"name":"Notifications::LiveCourseSessionNotificationBuilder"},{"name":"Notifications::LiveCourseUnregisteredFromCanceledSessionNotificationBuilder"},{"name":"Notifications::LiveCourseUnregisteredFromSessionNotificationBuilder"},{"name":"Notifications::LiveCourseUnregisteredFromUnattendedSessionNotificationBuilder"},{"name":"Notifications::ManagerCourseDigest"},{"name":"Notifications::NotificationBuilder"},{"name":"Notifications::TimeUntilSessionNotificationBuilder"},{"name":"Reports::CourseStatusReport"},{"name":"Reports::PassRateReport"},{"name":"Reports::ScoreDistributionReport"},{"name":"Scorm::Activity"},{"name":"Scorm::Attempt"},{"name":"Scorm::CourseUploadResponse"},{"name":"Scorm::CourseUploadStatusResponse"},{"name":"Scorm::Enrollment"},{"name":"Scorm::RegistrationListResponse"},{"name":"Scorm::RegistrationResponse"},{"name":"Search::CustomFieldValueSearch"},{"name":"Search::EnrollableSearch"}],"edges":[{"from":0,"to":36},{"from":1,"to":46},{"from":2,"to":46},{"from":2,"to":46},{"from":2,"to":13},{"from":2,"to":41},{"from":5,"to":8},{"from":5,"to":6},{"from":5,"to":7},{"from":6,"to":5},{"from":6,"to":47},{"from":7,"to":5},{"from":7,"to":14},{"from":8,"to":5},{"from":8,"to":10},{"from":10,"to":47},{"from":10,"to":8},{"from":10,"to":15},{"from":10,"to":46},{"from":11,"to":18},{"from":11,"to":43},{"from":13,"to":10},{"from":13,"to":47},{"from":13,"to":2},{"from":13,"to":2},{"from":14,"to":30},{"from":14,"to":15},{"from":14,"to":26},{"from":14,"to":43},{"from":14,"to":7},{"from":15,"to":14},{"from":15,"to":10},{"from":16,"to":47},{"from":18,"to":47},{"from":18,"to":11},{"from":23,"to":47},{"from":23,"to":24},{"from":23,"to":27},{"from":23,"to":26},{"from":24,"to":47},{"from":24,"to":23},{"from":24,"to":25},{"from":24,"to":28},{"from":25,"to":24},{"from":26,"to":23},{"from":26,"to":14},{"from":27,"to":23},{"from":27,"to":28},{"from":28,"to":47},{"from":28,"to":27},{"from":28,"to":24},{"from":29,"to":47},{"from":29,"to":47},{"from":30,"to":14},{"from":30,"to":47},{"from":36,"to":0},{"from":41,"to":2},{"from":41,"to":46},{"from":43,"to":14},{"from":43,"to":11},{"from":45,"to":47},{"from":46,"to":10},{"from":46,"to":41},{"from":46,"to":1},{"from":47,"to":13},{"from":47,"to":10},{"from":47,"to":6},{"from":47,"to":24},{"from":47,"to":28},{"from":47,"to":23},{"from":47,"to":45},{"from":47,"to":30},{"from":47,"to":18},{"from":47,"to":29},{"from":47,"to":29},{"from":5,"to":10},{"from":5,"to":47},{"from":5,"to":14},{"from":10,"to":5},{"from":10,"to":14},{"from":11,"to":47},{"from":13,"to":46},{"from":14,"to":10},{"from":14,"to":23},{"from":14,"to":47},{"from":14,"to":5},{"from":23,"to":47},{"from":24,"to":14},{"from":24,"to":27},{"from":27,"to":47},{"from":47,"to":10},{"from":47,"to":23},{"from":47,"to":14},{"from":47,"to":11},{"from":47,"to":47},{"from":47,"to":47},{"from":0,"to":0},{"from":0,"to":3},{"from":0,"to":48},{"from":0,"to":36},{"from":2,"to":49},{"from":2,"to":22},{"from":2,"to":41},{"from":5,"to":13},{"from":5,"to":10},{"from":6,"to":13},{"from":6,"to":2},{"from":10,"to":46},{"from":12,"to":30},{"from":12,"to":30},{"from":13,"to":46},{"from":13,"to":47},{"from":14,"to":10},{"from":14,"to":14},{"from":14,"to":47},{"from":27,"to":78},{"from":27,"to":78},{"from":27,"to":78},{"from":28,"to":78},{"from":31,"to":30},{"from":31,"to":30},{"from":31,"to":30},{"from":32,"to":86},{"from":32,"to":66},{"from":32,"to":66},{"from":33,"to":69},{"from":33,"to":69},{"from":34,"to":71},{"from":34,"to":78},{"from":34,"to":78},{"from":37,"to":78},{"from":37,"to":78},{"from":37,"to":47},{"from":37,"to":37},{"from":40,"to":40},{"from":40,"to":40},{"from":40,"to":55},{"from":40,"to":40},{"from":40,"to":40},{"from":40,"to":40},{"from":40,"to":40},{"from":41,"to":40},{"from":43,"to":63},{"from":45,"to":45},{"from":47,"to":47},{"from":47,"to":47},{"from":47,"to":5},{"from":47,"to":10},{"from":47,"to":14},{"from":47,"to":23},{"from":53,"to":40},{"from":54,"to":40},{"from":53,"to":40},{"from":56,"to":40},{"from":53,"to":55},{"from":57,"to":40},{"from":58,"to":40},{"from":57,"to":40},{"from":59,"to":40},{"from":60,"to":40},{"from":61,"to":40},{"from":60,"to":40},{"from":62,"to":40},{"from":77,"to":35},{"from":82,"to":82},{"from":82,"to":82},{"from":83,"to":83},{"from":83,"to":83},{"from":83,"to":78},{"from":86,"to":83},{"from":86,"to":83},{"from":90,"to":47},{"from":90,"to":14}]}
@@ -0,0 +1,174 @@
1
+ require 'spec_helper'
2
+
3
+ describe Society::AssociationProcessor do
4
+
5
+ describe "#associations" do
6
+
7
+ context "`belongs_to`, `has_many`, and `has_many through:` associations" do
8
+ let(:code) {
9
+ <<-CODE
10
+ class Assembly < ActiveRecord::Base
11
+ has_many :manifests
12
+ has_many :parts, through: :manifests
13
+ end
14
+
15
+ class Manifest < ActiveRecord::Base
16
+ belongs_to :assembly
17
+ belongs_to :part
18
+ end
19
+
20
+ class Part < ActiveRecord::Base
21
+ has_many :manifests
22
+ has_many :assemblies, through: :manifests
23
+ end
24
+ CODE
25
+ }
26
+
27
+ let(:parser) { Society::Parser.for_source(code) }
28
+ let(:processor) { Society::AssociationProcessor.new(parser.analyzer.classes) }
29
+ let(:assembly) { processor.classes.detect {|c| c.name == "Assembly" } }
30
+ let(:manifest) { processor.classes.detect {|c| c.name == "Manifest" } }
31
+ let(:part) { processor.classes.detect {|c| c.name == "Part" } }
32
+
33
+ it "records `has_many` and `has_many through:` associations" do
34
+ assembly_associations = processor.associations.select { |edge| edge.from == assembly }
35
+ expect(assembly_associations.map(&:to)).to match_array [manifest, part]
36
+
37
+ part_associations = processor.associations.select { |edge| edge.from == part }
38
+ expect(part_associations.map(&:to)).to eq [manifest, assembly]
39
+ end
40
+
41
+ it "records `belongs_to` associations" do
42
+ manifest_associations = processor.associations.select { |edge| edge.from == manifest }
43
+ expect(manifest_associations.map(&:to)).to match_array [assembly, part]
44
+ end
45
+ end
46
+
47
+ context "associations with `class_name` specified" do
48
+ let(:code) {<<-CODE
49
+ class Post < ActiveRecord::Base
50
+ belongs_to :author, :class_name => "User"
51
+ belongs_to :editor, :class_name => "User"
52
+ end
53
+
54
+ class User < ActiveRecord::Base
55
+ has_many :authored_posts, :foreign_key => "author_id", :class_name => "Post"
56
+ has_many :edited_posts, :foreign_key => "editor_id", :class_name => "Post"
57
+ end
58
+ CODE
59
+ }
60
+ let(:parser) { Society::Parser.for_source(code) }
61
+ let(:processor) { Society::AssociationProcessor.new(parser.analyzer.classes) }
62
+ let(:user) { processor.classes.detect {|c| c.name == "User" } }
63
+ let(:post) { processor.classes.detect {|c| c.name == "Post" } }
64
+
65
+ it "records belongs_to's" do
66
+ post_associations = processor.associations.select { |edge| edge.from == post }
67
+ expect(post_associations.map(&:to)).to match_array [user, user]
68
+ end
69
+
70
+ it "records has_many's" do
71
+ user_associations = processor.associations.select { |edge| edge.from == user }
72
+ expect(user_associations.map(&:to)).to match_array [post, post]
73
+ end
74
+ end
75
+
76
+ context "associations with `class_name` or `source` specified" do
77
+ let(:code) { <<-CODE
78
+ class Post < ActiveRecord::Base
79
+ has_many :post_authorings, :foreign_key => :authored_post_id
80
+ has_many :authors, :through => :post_authorings, :source => :post_author
81
+ belongs_to :editor, :class_name => "User"
82
+ end
83
+
84
+ class User < ActiveRecord::Base
85
+ has_many :post_authorings, :foreign_key => :post_author_id
86
+ has_many :authored_posts, :through => :post_authorings
87
+ has_many :edited_posts, :foreign_key => :editor_id, :class_name => "Post"
88
+ end
89
+
90
+ class PostAuthoring < ActiveRecord::Base
91
+ belongs_to :post_author, :class_name => "User"
92
+ belongs_to :authored_post, :class_name => "Post"
93
+ end
94
+ CODE
95
+ }
96
+ let(:parser) { Society::Parser.for_source(code) }
97
+ let(:processor) { Society::AssociationProcessor.new(parser.analyzer.classes) }
98
+ let(:user) { processor.classes.detect {|c| c.name == "User" } }
99
+ let(:post) { processor.classes.detect {|c| c.name == "Post" } }
100
+ let(:post_authoring) { processor.classes.detect {|c| c.name == "PostAuthoring" } }
101
+
102
+ it "handles `source` and `class_name` correctly" do
103
+ post_associations = processor.associations.select { |edge| edge.from == post }
104
+ expect(post_associations.map(&:to)).to match_array [post_authoring, user, user]
105
+ end
106
+
107
+ it "handles `through` and `class_name` correctly" do
108
+ user_associations = processor.associations.select { |edge| edge.from == user }
109
+ expect(user_associations.map(&:to)).to match_array [post_authoring, post, post]
110
+ end
111
+
112
+ it "handles `class_name` correctly" do
113
+ pa_associations = processor.associations.select { |edge| edge.from == post_authoring }
114
+ expect(pa_associations.map(&:to)).to match_array [user, post]
115
+ end
116
+ end
117
+
118
+ context "polymorphic associations" do
119
+ let(:code) { <<-CODE
120
+ class Picture < ActiveRecord::Base
121
+ belongs_to :imageable, polymorphic: true
122
+ end
123
+
124
+ class Employee < ActiveRecord::Base
125
+ has_many :pictures, as: :imageable
126
+ end
127
+
128
+ class Product < ActiveRecord::Base
129
+ has_many :pictures, as: :imageable
130
+ end
131
+ CODE
132
+ }
133
+ let(:parser) { Society::Parser.for_source(code) }
134
+ let(:processor) { Society::AssociationProcessor.new(parser.analyzer.classes) }
135
+ let(:picture) { processor.classes.detect {|c| c.name == "Picture" } }
136
+ let(:employee) { processor.classes.detect {|c| c.name == "Employee" } }
137
+ let(:product) { processor.classes.detect {|c| c.name == "Product" } }
138
+
139
+ it "records the `polymorphic: true` side of the association" do
140
+ picture_associations = processor.associations.select { |edge| edge.from == picture }
141
+ expect(picture_associations.map(&:to)).to match_array [employee, product]
142
+ end
143
+
144
+ it "records the `as: :something` side of the asociation" do
145
+ employee_associations = processor.associations.select { |edge| edge.from == employee }
146
+ expect(employee_associations.map(&:to)).to match_array [picture]
147
+
148
+ product_associations = processor.associations.select { |edge| edge.from == product }
149
+ expect(product_associations .map(&:to)).to match_array [picture]
150
+ end
151
+ end
152
+
153
+ context "self joins" do
154
+ let(:code) {<<-CODE
155
+ class Employee < ActiveRecord::Base
156
+ has_many :subordinates, class_name: "Employee",
157
+ foreign_key: "manager_id"
158
+
159
+ belongs_to :manager, class_name: "Employee"
160
+ end
161
+ CODE
162
+ }
163
+ let(:parser) { Society::Parser.for_source(code) }
164
+ let(:processor) { Society::AssociationProcessor.new(parser.analyzer.classes) }
165
+ let(:employee) { processor.classes.detect {|c| c.name == "Employee" } }
166
+
167
+ it "records self joins" do
168
+ employee_associations = processor.associations.select { |edge| edge.from == employee }
169
+ expect(employee_associations.map(&:to)).to match_array [employee, employee]
170
+ end
171
+ end
172
+ end
173
+ end
174
+
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/society/cli'
3
+
4
+ describe Society::CLI do
5
+
6
+ describe "#from" do
7
+
8
+ let(:parser) { Society::Parser.new(nil) }
9
+
10
+ it "invokes Society with a path" do
11
+ allow(parser).to receive(:report)
12
+ expect(Society).to receive(:new) { parser }
13
+ Society::CLI.new.from("./spec/fixtures")
14
+ end
15
+
16
+ it "calls report on a Parser instance" do
17
+ expect(parser).to receive(:report)
18
+ allow(Society).to receive(:new) { parser }
19
+ Society::CLI.new.from("./spec/fixtures")
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+