ahnnotate 0.2.0

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +55 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +118 -0
  8. data/Rakefile +10 -0
  9. data/ahnnotate.gemspec +33 -0
  10. data/bin/console +14 -0
  11. data/bin/rake +29 -0
  12. data/bin/setup +8 -0
  13. data/exe/ahnnotate +14 -0
  14. data/lib/ahnnotate/cli.rb +116 -0
  15. data/lib/ahnnotate/column.rb +60 -0
  16. data/lib/ahnnotate/config.rb +67 -0
  17. data/lib/ahnnotate/error.rb +8 -0
  18. data/lib/ahnnotate/facet/models/main.rb +49 -0
  19. data/lib/ahnnotate/facet/models/module_node.rb +123 -0
  20. data/lib/ahnnotate/facet/models/processor.rb +91 -0
  21. data/lib/ahnnotate/facet/models/resolve_active_record_models.rb +42 -0
  22. data/lib/ahnnotate/facet/models/resolve_class_relationships.rb +67 -0
  23. data/lib/ahnnotate/facet/models/standin.rb +41 -0
  24. data/lib/ahnnotate/facet/models.rb +10 -0
  25. data/lib/ahnnotate/function/format.rb +36 -0
  26. data/lib/ahnnotate/function/main.rb +32 -0
  27. data/lib/ahnnotate/function/run.rb +24 -0
  28. data/lib/ahnnotate/function/tabularize.rb +53 -0
  29. data/lib/ahnnotate/index.rb +26 -0
  30. data/lib/ahnnotate/options.rb +48 -0
  31. data/lib/ahnnotate/rails.rake +44 -0
  32. data/lib/ahnnotate/railtie.rb +7 -0
  33. data/lib/ahnnotate/table.rb +44 -0
  34. data/lib/ahnnotate/tables.rb +55 -0
  35. data/lib/ahnnotate/version.rb +3 -0
  36. data/lib/ahnnotate/vfs.rb +69 -0
  37. data/lib/ahnnotate/vfs_driver/filesystem.rb +68 -0
  38. data/lib/ahnnotate/vfs_driver/hash.rb +59 -0
  39. data/lib/ahnnotate/vfs_driver/read_only_filesystem.rb +8 -0
  40. data/lib/ahnnotate.rb +32 -0
  41. metadata +202 -0
@@ -0,0 +1,123 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ # ModuleNode is named as such since `Class.is_a?(Module) == true`.
5
+ class ModuleNode
6
+ # By including ClassMethods this way, I'm including the methods as
7
+ # instance methods. I'm doing this so that I can compute the table name
8
+ # on per-class basis.
9
+ #
10
+ # It's a bit unfortunate that this class will be used to both (1) keep
11
+ # track of how classes/modules relate to each other and (2) compute the
12
+ # table name.
13
+ include ActiveRecord::ModelSchema::ClassMethods
14
+
15
+ # Named to fit the ModelSchema interface. This is basically `Class#name`
16
+ attr_accessor :name
17
+ # Named to fit the ModelSchema interface. This is the "outer class"
18
+ attr_accessor :parent
19
+ # Named to fit the ModelSchema interface. This is the class that the
20
+ # current class inherits from. This is computed, whereas
21
+ # `claimed_superclass` is what is parsed from the source
22
+ attr_accessor :superclass
23
+ # Named to fit the ModelSchema interface. This is currently unsupported
24
+ attr_accessor :table_name_prefix
25
+
26
+ attr_writer :claimed_superclass
27
+ attr_writer :abstract_class
28
+ attr_writer :is_a_kind_of_activerecord_base
29
+ attr_accessor :explicit_table_name
30
+ attr_accessor :is_active_record_base
31
+ attr_accessor :path
32
+
33
+ def initialize(name,
34
+ parent: nil,
35
+ is_a_kind_of_activerecord_base: false,
36
+ claimed_superclass: nil,
37
+ explicit_table_name: nil,
38
+ abstract_class: nil)
39
+ self.name = name
40
+ self.parent = parent
41
+ self.is_a_kind_of_activerecord_base = is_a_kind_of_activerecord_base
42
+ self.claimed_superclass = claimed_superclass
43
+ self.explicit_table_name = explicit_table_name
44
+ self.abstract_class = abstract_class
45
+ end
46
+
47
+ # Named to fit the ModelSchema interface
48
+ def pluralize_table_names
49
+ true
50
+ end
51
+
52
+ def table_name_suffix
53
+ end
54
+
55
+ def is_a_kind_of_activerecord_base?
56
+ !!@is_a_kind_of_activerecord_base
57
+ end
58
+
59
+ # Named to fit the ModelSchema interface
60
+ def abstract_class?
61
+ !!@abstract_class
62
+ end
63
+
64
+ def claimed_superclass
65
+ @claimed_superclass.to_s
66
+ end
67
+
68
+ # Named to fit the ModelSchema interface. It was originally implemented
69
+ # in ActiveRecord::Inheritance. I've re-implemented it here via the
70
+ # documentation.
71
+ def base_class
72
+ if superclass.is_active_record_base
73
+ return self
74
+ end
75
+
76
+ if superclass.abstract_class?
77
+ return self
78
+ end
79
+
80
+ superclass.base_class
81
+ end
82
+
83
+ # Named to fit the ModelSchema interface. It was originally implemented
84
+ # in ActiveSupport::Introspection
85
+ def parents
86
+ if parent
87
+ [parent, *parent.parent]
88
+ else
89
+ []
90
+ end
91
+ end
92
+
93
+ def class_name
94
+ if @name
95
+ "#{parent.class_name}::#{@name}"
96
+ else
97
+ ""
98
+ end
99
+ end
100
+
101
+ def table_name
102
+ if explicit_table_name
103
+ return @explicit_table_name
104
+ end
105
+
106
+ super
107
+ end
108
+
109
+ def <(other)
110
+ other == ActiveRecord::Base && is_a_kind_of_activerecord_base?
111
+ end
112
+
113
+ def ==(other)
114
+ if is_active_record_base && other == ActiveRecord::Base
115
+ return true
116
+ end
117
+
118
+ super
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,91 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ class Processor < Parser::AST::Processor
5
+ def call(content)
6
+ sexp = Parser::CurrentRuby.parse(content)
7
+
8
+ @current_class = ModuleNode.new(nil)
9
+ @classes = [@current_class]
10
+
11
+ process(sexp)
12
+
13
+ @classes.reject { |klass| klass.class_name == "" }
14
+ end
15
+
16
+ def on_class(node)
17
+ @current_class = module_node_create(node, parent: @current_class)
18
+ @classes.push(@current_class)
19
+
20
+ super
21
+
22
+ @current_class = @current_class.parent
23
+ end
24
+
25
+ alias on_module on_class
26
+
27
+ def on_send(node)
28
+ receiver, method_name, assigned_node = *node
29
+
30
+ if receiver == s(:self) && method_name == :table_name=
31
+ table_name = assigned_node.children.last
32
+ @current_class.table_name = table_name
33
+ end
34
+
35
+ if receiver == s(:self) && method_name == :abstract_class=
36
+ abstract_class =
37
+ if assigned_node.type == :true
38
+ true
39
+ else
40
+ false
41
+ end
42
+ @current_class.abstract_class = abstract_class
43
+ end
44
+ end
45
+
46
+ # ignore instance method definitions since method definition bodies
47
+ # can't contain class declarations
48
+ def on_def(_)
49
+ end
50
+
51
+ # ignore class method definitions since method definition bodies can't
52
+ # contain class declarations
53
+ def on_defs(_)
54
+ end
55
+
56
+ private
57
+
58
+ def module_node_create(node, parent:)
59
+ class_node, superclass_node, _body_node = *node
60
+
61
+ if node.type == :module
62
+ superclass_node = nil
63
+ end
64
+
65
+ class_name = resolve_class_name(class_node)
66
+ superclass_name = resolve_class_name(superclass_node)
67
+
68
+ classlike = ModuleNode.new(class_name.to_s)
69
+ classlike.claimed_superclass = superclass_name.to_s
70
+ classlike.parent = parent
71
+
72
+ classlike
73
+ end
74
+
75
+ def resolve_class_name(node)
76
+ outer, name = *node
77
+
78
+ if outer.nil?
79
+ name
80
+ else
81
+ "#{resolve_class_name(outer)}::#{name}"
82
+ end
83
+ end
84
+
85
+ def s(type, *children)
86
+ AST::Node.new(type, children)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,42 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ class ResolveActiveRecordModels
5
+ include ProcParty
6
+
7
+ def call(object_space)
8
+ tree = {}
9
+ object_space.each do |_class_name, extracted_class|
10
+ superclass = extracted_class.superclass
11
+
12
+ tree[superclass] ||= Set.new
13
+ tree[superclass].add(extracted_class)
14
+ end
15
+
16
+ activerecord_family =
17
+ gather_family(object_space["::ActiveRecord::Base"], tree)
18
+
19
+ activerecord_family.each do |individual|
20
+ individual.is_a_kind_of_activerecord_base = true
21
+ end
22
+
23
+ object_space.values - [object_space[""], object_space["::ActiveRecord::Base"]]
24
+ end
25
+
26
+ private
27
+
28
+ def gather_family(node, tree)
29
+ gather_family_helper(node, tree).flatten
30
+ end
31
+
32
+ def gather_family_helper(node, tree)
33
+ if tree[node].nil?
34
+ return node
35
+ end
36
+
37
+ [node] + tree[node].map { |child| gather_family_helper(child, tree) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,67 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ class ResolveClassRelationships
5
+ include ProcParty
6
+
7
+ def call(extracted_classes)
8
+ object_space =
9
+ extracted_classes
10
+ .map(&method(:self_and_outer_class))
11
+ .flatten
12
+ .compact
13
+ .uniq
14
+ .map { |x| [x.class_name, x] }
15
+ .to_h
16
+
17
+ object_space[""] ||= ModuleNode.new(nil)
18
+
19
+ object_space["::ActiveRecord::Base"] =
20
+ ModuleNode.new(
21
+ "ActiveRecord::Base",
22
+ parent: object_space[""],
23
+ abstract_class: nil
24
+ )
25
+ object_space["::ActiveRecord::Base"].is_active_record_base = true
26
+
27
+ object_space.each do |class_name, extracted_class|
28
+ possible_namespace_levels = class_name.split("::")[1..-2] || []
29
+
30
+ if extracted_class.claimed_superclass == "" || extracted_class.claimed_superclass == nil
31
+ next
32
+ end
33
+
34
+ (possible_namespace_levels.size + 1).times do
35
+ class_uri_parts =
36
+ possible_namespace_levels + [extracted_class.claimed_superclass]
37
+
38
+ class_uri = "::#{class_uri_parts.join("::")}"
39
+
40
+ if object_space[class_uri]
41
+ extracted_class.superclass = object_space[class_uri]
42
+ break
43
+ end
44
+
45
+ possible_namespace_levels.pop
46
+ end
47
+ end
48
+
49
+ object_space
50
+ end
51
+
52
+ private
53
+
54
+ def self_and_outer_class(extracted_class)
55
+ if extracted_class.nil?
56
+ return nil
57
+ end
58
+
59
+ [
60
+ extracted_class,
61
+ self_and_outer_class(extracted_class.parent)
62
+ ]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ class Standin
5
+ # Lol. It's a little easier in this case to deal with instances here.
6
+ # I'll need to create many instances to recreate the class structure of
7
+ # the models in order to most correctly compute the table name from the
8
+ # model name 🥳
9
+ include ActiveRecord::ModelSchema::ClassMethods
10
+
11
+ # boolean, is this an abstract class
12
+ attr_writer :abstract_class
13
+ # the class that the current class inherits from
14
+ attr_accessor :superclass
15
+ attr_accessor :base_class
16
+ # the outer class
17
+ attr_accessor :parent
18
+
19
+ def initialize(abstract_class:, superclass:, base_class:, parent:, is_active_record_base: false)
20
+ @abstract_class = abstract_class
21
+ @superclass = superclass
22
+ @base_class = base_class
23
+ @parent = parent
24
+ @is_active_record_base = is_active_record_base
25
+ end
26
+
27
+ def abstract_class?
28
+ !!@abstract_class
29
+ end
30
+
31
+ def ==(other)
32
+ if @is_active_record_base && other == ActiveRecord::Base
33
+ return true
34
+ end
35
+
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ module Ahnnotate
2
+ module Facet
3
+ module Models
4
+ def self.add(config, tables, vfs)
5
+ runner = Main.new(config, tables, vfs)
6
+ runner.call
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ module Ahnnotate
2
+ module Function
3
+ class Format
4
+ attr_reader :comment
5
+
6
+ def initialize(comment:)
7
+ @comment = comment
8
+ end
9
+
10
+ def call(table, content)
11
+ table.string(comment: comment) + "\n" + strip_schema(content)
12
+ end
13
+
14
+ private
15
+
16
+ def strip_schema(content)
17
+ matches = pattern.match(content)
18
+
19
+ if matches
20
+ matches["post"]
21
+ else
22
+ content
23
+ end
24
+ end
25
+
26
+ def pattern
27
+ @pattern ||=
28
+ begin
29
+ newline = /\r?\n\r?/
30
+
31
+ /\A#{comment}\s==\sSchema\sInfo#{newline}?(?:^#{comment}[^\n]*$#{newline})*#{newline}(?<post>.*)/m
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ module Ahnnotate
2
+ module Function
3
+ class Main
4
+ def initialize(root, options, config)
5
+ @root = root
6
+ @options = options
7
+ @config = config
8
+ end
9
+
10
+ def call
11
+ if @config["boot"]
12
+ eval @config["boot"]
13
+ end
14
+
15
+ vfs = Vfs.new(vfs_driver)
16
+
17
+ runner = Run.new(@config, vfs)
18
+ runner.call
19
+ end
20
+
21
+ private
22
+
23
+ def vfs_driver
24
+ if @options.fix?
25
+ VfsDriver::Filesystem.new(root: @root)
26
+ else
27
+ VfsDriver::ReadOnlyFilesystem.new(root: @root)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ module Ahnnotate
2
+ module Function
3
+ class Run
4
+ def initialize(config, vfs)
5
+ @config = config
6
+ @vfs = vfs
7
+ end
8
+
9
+ def call
10
+ Facet::Models.add(@config, tables_hash, @vfs)
11
+ end
12
+
13
+ private
14
+
15
+ def tables_hash
16
+ @tables_hash = tables.to_h
17
+ end
18
+
19
+ def tables
20
+ @tables ||= Tables.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ module Ahnnotate
2
+ module Function
3
+ class Tabularize
4
+ def initialize(prefix:, cell_divider:)
5
+ @prefix = prefix
6
+ @cell_divider = cell_divider
7
+ end
8
+
9
+ def call(data, column_names)
10
+ output = StringIO.new
11
+ minimum_column_lengths = Hash.new { 0 }
12
+
13
+ rows = data.map do |row|
14
+ row_hash = {}
15
+
16
+ column_names.each do |c|
17
+ value = row.public_send(c).to_s
18
+
19
+ row_hash[c] = value
20
+
21
+ if value.size > minimum_column_lengths[c]
22
+ minimum_column_lengths[c] = value.size
23
+ end
24
+ end
25
+
26
+ row_hash
27
+ end
28
+
29
+ rows.each do |row|
30
+ # Note: minimum_column_lengths shouldn't include any of the columns
31
+ # with a length of zero since they were never explicitly set (to 0)
32
+ minimum_column_lengths.each.with_index do |(column_name, column_max_length), index|
33
+ if index == 0
34
+ output.print(@prefix)
35
+ end
36
+
37
+ if_rightmost_column = index + 1 == minimum_column_lengths.size
38
+
39
+ if if_rightmost_column
40
+ output.puts "#{row[column_name]}"
41
+ else
42
+ column_length = row[column_name].size
43
+ spaces_length = column_max_length - column_length
44
+ output.print "#{row[column_name]}#{" " * spaces_length}#{@cell_divider}"
45
+ end
46
+ end
47
+ end
48
+
49
+ output.string.gsub(/ +$/, "")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ module Ahnnotate
2
+ class Index
3
+ attr_accessor :name
4
+ attr_accessor :columns
5
+ attr_accessor :comment
6
+ attr_accessor :unique
7
+
8
+ def initialize(**args)
9
+ args.each do |key, value|
10
+ public_send("#{key}=", value)
11
+ end
12
+ end
13
+
14
+ def presentable_columns
15
+ "(#{columns.join(", ")})"
16
+ end
17
+
18
+ def presentable_unique
19
+ if unique
20
+ "UNIQUE"
21
+ else
22
+ ""
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ module Ahnnotate
2
+ class Options
3
+ def self.attribute_names
4
+ @attribute_names ||= []
5
+ end
6
+
7
+ def self.attr_writer(*names)
8
+ attribute_names.push(*names)
9
+ super
10
+ end
11
+
12
+ def self.attr_question(*names)
13
+ names.each do |name|
14
+ attr_writer(name)
15
+
16
+ define_method("#{name}?") do
17
+ !!instance_variable_get("@#{name}")
18
+ end
19
+ end
20
+ end
21
+
22
+ attr_question :exit
23
+ attr_question :fix
24
+
25
+ def initialize(**args)
26
+ args.each do |key, value|
27
+ public_send("#{key}=", value)
28
+ end
29
+ end
30
+
31
+ def to_s
32
+ output = StringIO.new
33
+
34
+ output.puts "🧐 options:"
35
+ self.class.attribute_names.each do |attribute_name|
36
+ output.print "🧐 #{attribute_name}: "
37
+
38
+ if instance_variable_defined?("@#{attribute_name}")
39
+ output.puts "undefined"
40
+ else
41
+ output.puts instance_variable_get("@#{attribute_name}").inspect
42
+ end
43
+ end
44
+
45
+ output.string
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ namespace :db do
2
+ task :migrate do
3
+ $rake_ahnnotate_config ||= Ahnnotate::Config.load(root: Rails.root)
4
+
5
+ if $rake_ahnnotate_config["rake_db_autorun"]
6
+ Rake::Task["ahnnotate:all"].reenable
7
+ Rake::Task["ahnnotate:all"].invoke
8
+ end
9
+ end
10
+
11
+ task :rollback do
12
+ $rake_ahnnotate_config ||= Ahnnotate::Config.load(root: Rails.root)
13
+
14
+ if $rake_ahnnotate_config["rake_db_autorun"]
15
+ Rake::Task["ahnnotate:all"].reenable
16
+ Rake::Task["ahnnotate:all"].invoke
17
+ end
18
+ end
19
+ end
20
+
21
+ namespace :ahnnotate do
22
+ desc "Run ahnnotate"
23
+ task :all do
24
+ require "ahnnotate/cli"
25
+ require "shellwords"
26
+
27
+ # This should either be `rails` or `rake` (since newer versions of Rails
28
+ # can call rake tasks with either executable)
29
+ exe_name = File.basename($0)
30
+
31
+ argv = ENV.fetch("COMMENTATE", "--fix")
32
+ argv = Shellwords.split(argv)
33
+
34
+ puts "Commentating models..."
35
+
36
+ cli = Ahnnotate::Cli.new(name: "#{exe_name} ahnnotate")
37
+ cli.run(argv, $rake_ahnnotate_config)
38
+
39
+ puts "Done!"
40
+ end
41
+ end
42
+
43
+ desc "Run rake task `ahnnotate:all`"
44
+ task ahnnotate: "ahnnotate:all"
@@ -0,0 +1,7 @@
1
+ module Ahnnotate
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "ahnnotate/rails.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ module Ahnnotate
2
+ class Table
3
+ attr_accessor :name
4
+ attr_accessor :columns
5
+ attr_accessor :indexes
6
+
7
+ def initialize(**args)
8
+ args.each do |key, value|
9
+ public_send("#{key}=", value)
10
+ end
11
+ end
12
+
13
+ def string(comment:)
14
+ tabularizer =
15
+ Function::Tabularize.new(
16
+ prefix: "#{comment} ",
17
+ cell_divider: " "
18
+ )
19
+
20
+ output = StringIO.new
21
+ output.puts "#{comment} == Schema Info"
22
+ output.puts comment
23
+ output.puts "#{comment} Table name: #{@name}"
24
+ output.puts comment
25
+ output.print tabularizer.call(columns, [:name, :type, :details])
26
+ output.puts comment
27
+
28
+ if indexes.any?
29
+ output.puts "#{comment} Indexes:"
30
+ output.puts comment
31
+ output.print tabularizer.call(indexes, [:name, :presentable_columns, :presentable_unique, :comment])
32
+ output.puts comment
33
+ end
34
+
35
+ output.string
36
+ end
37
+
38
+ private
39
+
40
+ def longest_column_name_length
41
+ @longest_column_name_length ||= @columns.map(&:name).map(&:size).max
42
+ end
43
+ end
44
+ end