ginny 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +138 -0
- data/.solargraph.yml +14 -0
- data/.travis.yml +7 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +68 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/Rakefile +10 -0
- data/bin/console +9 -0
- data/bin/docs +2 -0
- data/bin/setup +8 -0
- data/exe/ginny +23 -0
- data/ginny.gemspec +43 -0
- data/lib/ginny.rb +6 -0
- data/lib/ginny/error.rb +5 -0
- data/lib/ginny/load.rb +45 -0
- data/lib/ginny/mod.rb +32 -0
- data/lib/ginny/models/attr.rb +73 -0
- data/lib/ginny/models/class.rb +94 -0
- data/lib/ginny/models/func.rb +86 -0
- data/lib/ginny/models/param.rb +90 -0
- data/lib/ginny/string/comment.rb +19 -0
- data/lib/ginny/string/indent.rb +52 -0
- data/lib/ginny/symbolize.rb +31 -0
- data/lib/ginny/version.rb +3 -0
- metadata +203 -0
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/docs
ADDED
data/bin/setup
ADDED
data/exe/ginny
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "ginny"
|
4
|
+
require "optparse"
|
5
|
+
options = {}
|
6
|
+
|
7
|
+
optparse = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: ginny [options] [path]"
|
9
|
+
options[:verbose] = false
|
10
|
+
opts.on("-v", "--verbose", "Output more information") do
|
11
|
+
options[:verbose] = true
|
12
|
+
end
|
13
|
+
opts.on("-h", "--help", "Display this screen") do
|
14
|
+
puts(opts)
|
15
|
+
exit(0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
optparse.parse!
|
20
|
+
|
21
|
+
ARGV.each do |f|
|
22
|
+
Ginny::Class.create(Ginny.load_file(f)).generate()
|
23
|
+
end
|
data/ginny.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "ginny/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "ginny"
|
7
|
+
spec.version = Ginny::VERSION
|
8
|
+
spec.required_ruby_version = ">= 2.3.0"
|
9
|
+
spec.description = "Generate Ruby code."
|
10
|
+
spec.summary = spec.description
|
11
|
+
spec.authors = ["Clay Dunston"]
|
12
|
+
spec.email = ["dunstontc@gmail.com"]
|
13
|
+
spec.homepage = "https://github.com/tcd/ginny"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.metadata = {
|
17
|
+
"homepage_uri" => spec.homepage,
|
18
|
+
"source_code_uri" => spec.homepage,
|
19
|
+
"documentation_uri" => "https://www.rubydoc.info/gems/ginny/#{spec.version}",
|
20
|
+
"changelog_uri" => "https://github.com/tcd/ginny/blob/master/CHANGELOG.md",
|
21
|
+
"yard.run" => "yri", # use "yard" to build full HTML docs.
|
22
|
+
}
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = ["ginny"]
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
34
|
+
spec.add_development_dependency "coveralls", "~> 0.8.23"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "minitest-focus", "~> 1.1"
|
37
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.4"
|
38
|
+
spec.add_development_dependency "pry", "~> 0.12.2"
|
39
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
40
|
+
spec.add_development_dependency "simplecov"
|
41
|
+
|
42
|
+
spec.add_runtime_dependency "dry-inflector", "~> 0.2.0"
|
43
|
+
end
|
data/lib/ginny.rb
ADDED
data/lib/ginny/error.rb
ADDED
data/lib/ginny/load.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "json"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Ginny
|
6
|
+
|
7
|
+
# Load data from a YAML file.
|
8
|
+
#
|
9
|
+
# @param path [String]
|
10
|
+
# @return [Hash<Symbol>]
|
11
|
+
def self.load_yaml(path)
|
12
|
+
return Ginny.symbolize_keys(YAML.load_file(File.expand_path(path)))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load data from a JSON file.
|
16
|
+
#
|
17
|
+
# @param path [String]
|
18
|
+
# @return [Hash<Symbol>]
|
19
|
+
def self.load_json(path)
|
20
|
+
return JSON.parse(File.read(File.expand_path(path)), symbolize_names: true)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param path [String] Path of the file to determine the extension of.
|
24
|
+
# @return [String]
|
25
|
+
def self.get_extension(path)
|
26
|
+
file = Pathname.new(path)
|
27
|
+
return file.extname.downcase
|
28
|
+
end
|
29
|
+
|
30
|
+
# Load data from a YAML or JSON file.
|
31
|
+
#
|
32
|
+
# @param path [String]
|
33
|
+
# @return [Hash<Symbol>]
|
34
|
+
def self.load_file(path)
|
35
|
+
case Pathname.new(path).extname.downcase
|
36
|
+
when ".yml", ".yaml"
|
37
|
+
return self.load_yaml(path)
|
38
|
+
when ".json"
|
39
|
+
return self.load_json(path)
|
40
|
+
else
|
41
|
+
raise Ginny::Error "invalid file type"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/lib/ginny/mod.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ginny
|
2
|
+
|
3
|
+
# Used to generate a [module](https://ruby-doc.org/core-2.6.5/doc/syntax/modules_and_classes_rdoc.html).
|
4
|
+
#
|
5
|
+
# More accurately, wrap the `body` (first argument) with any following module definitions (additional arguments).
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Ginny.mod("puts('Hello World')", "Level1", "Level2")
|
9
|
+
# #=> module Level1
|
10
|
+
# module Level2
|
11
|
+
# puts('Hello World')
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @param body [String] Name of module namespaces.
|
16
|
+
# @param names [String,Array<String>] Name of module namespaces.
|
17
|
+
# @return [String]
|
18
|
+
def self.mod(body, *names)
|
19
|
+
names.flatten!
|
20
|
+
count = names.length
|
21
|
+
return body unless count.positive?()
|
22
|
+
level = 0
|
23
|
+
head = []
|
24
|
+
tail = []
|
25
|
+
names.each do |name|
|
26
|
+
head.push("module #{name}".indent(level))
|
27
|
+
tail.unshift("end".indent(level))
|
28
|
+
level += 2
|
29
|
+
end
|
30
|
+
return (head + [body&.indent(level)] + tail).compact.join("\n")
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Ginny
|
2
|
+
# Used to generate an instance variable with getters/setters.
|
3
|
+
#
|
4
|
+
# [attr]: https://docs.ruby-lang.org/en/master/Module.html#method-i-attr
|
5
|
+
# [attr_accessor]: https://docs.ruby-lang.org/en/master/Module.html#method-i-attr_accessor
|
6
|
+
# [attr_reader]: https://docs.ruby-lang.org/en/master/Module.html#method-i-attr_reader
|
7
|
+
class Attr
|
8
|
+
|
9
|
+
# Name of the attribute.
|
10
|
+
# @return [String]
|
11
|
+
attr_accessor :name
|
12
|
+
# Description of the attribute. [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) is supported.
|
13
|
+
# @return [String]
|
14
|
+
attr_accessor :description
|
15
|
+
# [Type](https://rubydoc.info/gems/yard/file/docs/GettingStarted.md#Declaring_Types) of the attribute.
|
16
|
+
# @return [String]
|
17
|
+
attr_accessor :type
|
18
|
+
# Default value for the attribute; set in it's Class's `initialize` function.
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :default
|
21
|
+
# If `true`, an `attr_reader` will be generated for the attribute instead of an `attr_accessor`.
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_accessor :read_only
|
24
|
+
|
25
|
+
# @return [void]
|
26
|
+
def initialize()
|
27
|
+
self.read_only = false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Constructor for an Attr. Use `create`, not `new`.
|
31
|
+
#
|
32
|
+
# @param args [Hash]
|
33
|
+
# @return [Attr]
|
34
|
+
def self.create(args = {})
|
35
|
+
a = Ginny::Attr.new()
|
36
|
+
a.name = args[:name]
|
37
|
+
a.description = args[:description]
|
38
|
+
a.type = args[:type]
|
39
|
+
a.read_only = args[:read_only]
|
40
|
+
return a
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param array [Array<Hash>]
|
44
|
+
# @return [Array<Ginny::Attr>]
|
45
|
+
def self.from_array(array)
|
46
|
+
return array.map { |f| self.create(f) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
def render()
|
51
|
+
parts = []
|
52
|
+
parts << (@description&.length&.positive? ? @description.comment : nil)
|
53
|
+
parts << "@return [#{self.type}]".comment
|
54
|
+
parts << "attr_#{self.read_only ? 'reader' : 'accessor'} :#{self.name.downcase}"
|
55
|
+
return parts.compact.join("\n").gsub(/\s+$/, "")
|
56
|
+
end
|
57
|
+
|
58
|
+
# Used for documenting attributes that are "declared dynamically via meta-programming".
|
59
|
+
# See the documentation on [YARD directives][1] for more info.
|
60
|
+
#
|
61
|
+
# [1]: https://www.rubydoc.info/gems/yard/file/docs/Tags.md#attribute
|
62
|
+
#
|
63
|
+
# @return [String]
|
64
|
+
def render_dynamic()
|
65
|
+
parts = []
|
66
|
+
parts << "@!attribute #{self.name.downcase} [#{self.read_only ? 'r' : 'rw'}]".comment
|
67
|
+
parts << (@description&.length&.positive? ? @description.indent(2).comment : nil)
|
68
|
+
parts << "@return [#{self.type}]".indent(2).comment
|
69
|
+
return parts.compact.join("\n").gsub(/\s+$/, "")
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "dry/inflector"
|
2
|
+
|
3
|
+
module Ginny
|
4
|
+
# Used to generate a [class](https://ruby-doc.org/core-2.6.5/Class.html)
|
5
|
+
class Class
|
6
|
+
|
7
|
+
# Name of the class.
|
8
|
+
# @return [String]
|
9
|
+
attr_accessor :name
|
10
|
+
# Description of the class. [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) is supported.
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :description
|
13
|
+
# Name of a class to inherit from. (Ex: `YourNewClass < Parent`)
|
14
|
+
# @return [String]
|
15
|
+
attr_accessor :parent
|
16
|
+
# List of modules to declare the class inside.
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :modules
|
19
|
+
# An array of {Ginny::Attr}s.
|
20
|
+
# @return [Array<Ginny::Attr>]
|
21
|
+
attr_accessor :attrs
|
22
|
+
# String to write into the body of the class.
|
23
|
+
# @return [String]
|
24
|
+
attr_accessor :body
|
25
|
+
# String to prepend to the name of the generated file.
|
26
|
+
# @return [String]
|
27
|
+
attr_accessor :file_prefix
|
28
|
+
|
29
|
+
# @return [void]
|
30
|
+
def initialize()
|
31
|
+
self.attrs = []
|
32
|
+
self.modules = []
|
33
|
+
self.file_prefix = ""
|
34
|
+
end
|
35
|
+
|
36
|
+
# Constructor for a Class. Use `create`, not `new`.
|
37
|
+
#
|
38
|
+
# @param args [Hash<Symbol>]
|
39
|
+
# @return [Class]
|
40
|
+
def self.create(args = {})
|
41
|
+
c = Ginny::Class.new()
|
42
|
+
c.name = args[:name]
|
43
|
+
c.description = args[:description]
|
44
|
+
c.parent = args[:parent]
|
45
|
+
c.modules = args[:modules] unless args[:modules].nil?
|
46
|
+
c.attrs = Ginny::Attr.from_array(args[:attrs]) if args[:attrs]&.is_a?(Array)
|
47
|
+
c.body = args[:body] unless args[:body].nil?
|
48
|
+
c.file_prefix = args[:file_prefix] || ""
|
49
|
+
return c
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param folder [String]
|
53
|
+
# @return [String]
|
54
|
+
def generate(folder = ".")
|
55
|
+
path = File.join(File.expand_path(folder), self.file_name())
|
56
|
+
File.open(path, "a") { |f| f.write(self.render() + "\n") }
|
57
|
+
return path
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String]
|
61
|
+
def render()
|
62
|
+
parts = []
|
63
|
+
parts << (self.description&.length&.positive? ? self.description.comment.strip : nil)
|
64
|
+
parts << (self.parent.nil? ? "class #{self.class_name()}" : "class #{self.class_name()} < #{self.parent}")
|
65
|
+
parts << self.render_attributes()
|
66
|
+
parts << (self.body&.length&.positive? ? self.body.indent(2) : nil)
|
67
|
+
parts << "end"
|
68
|
+
if self.modules.length > 0
|
69
|
+
body = parts.compact.join("\n").gsub(/\s+$/, "")
|
70
|
+
return Ginny.mod(body, self.modules)
|
71
|
+
end
|
72
|
+
return parts.compact.join("\n").gsub(/\s+$/, "")
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [String]
|
76
|
+
def render_attributes()
|
77
|
+
return nil unless self.attrs.length > 0
|
78
|
+
return self.attrs.map(&:render).join("\n").indent(2)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def class_name()
|
83
|
+
inflector = Dry::Inflector.new
|
84
|
+
return inflector.classify(inflector.underscore(self.name))
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [String]
|
88
|
+
def file_name()
|
89
|
+
inflector = Dry::Inflector.new
|
90
|
+
return self.file_prefix + inflector.underscore(self.name) + ".rb"
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Ginny
|
2
|
+
# Used to generate a [method][2].
|
3
|
+
#
|
4
|
+
# [1]: https://ruby-doc.org/core-2.6.5/Method.html
|
5
|
+
# [2]: https://ruby-doc.org/core-2.6.5/doc/syntax/methods_rdoc.html
|
6
|
+
class Func
|
7
|
+
|
8
|
+
# Name of the function.
|
9
|
+
# @return [String]
|
10
|
+
attr_accessor :name
|
11
|
+
# Description of the function. [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) is supported.
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :description
|
14
|
+
# Return [type](https://rubydoc.info/gems/yard/file/docs/GettingStarted.md#Declaring_Types) of the function.
|
15
|
+
# @return [String]
|
16
|
+
attr_accessor :return_type
|
17
|
+
# String to write into the body of the function.
|
18
|
+
# @return [String]
|
19
|
+
attr_accessor :body
|
20
|
+
# List of modules to declare the function inside of.
|
21
|
+
# @return [String]
|
22
|
+
attr_accessor :modules
|
23
|
+
# An array of {Ginny::Param}s.
|
24
|
+
# @return [Array<Param>]
|
25
|
+
attr_accessor :params
|
26
|
+
|
27
|
+
# @return [void]
|
28
|
+
def initialize()
|
29
|
+
self.params = []
|
30
|
+
self.modules = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Constructor for a Func. Use `create`, not `new`.
|
34
|
+
#
|
35
|
+
# @param args [Hash<Symbol>]
|
36
|
+
# @return [Ginny::Func]
|
37
|
+
def self.create(args = {})
|
38
|
+
f = Ginny::Func.new()
|
39
|
+
f.name = args[:name]
|
40
|
+
f.description = args[:description]
|
41
|
+
f.return_type = args[:return_type]
|
42
|
+
f.body = args[:body]
|
43
|
+
f.modules = args[:modules] unless args[:modules].nil?
|
44
|
+
f.params = Ginny::Param.from_array(args[:params]) if args[:params]&.is_a?(Array)
|
45
|
+
return f
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String]
|
49
|
+
def render()
|
50
|
+
# return self.render_compact() if self.body.nil? && self.params.length == 0
|
51
|
+
parts = []
|
52
|
+
parts << self.render_description()
|
53
|
+
parts << self.params.map(&:render_doc).join("\n") unless self.params.length == 0
|
54
|
+
parts << self.render_return_type()
|
55
|
+
parts << "def " + self.name + self.render_params()
|
56
|
+
parts << self.body.indent(2) unless self.body.nil?
|
57
|
+
parts << "end"
|
58
|
+
|
59
|
+
body = parts.compact.join("\n").gsub(/\s+$/, "")
|
60
|
+
|
61
|
+
return Ginny.mod(body, self.modules) if self.modules.length > 0
|
62
|
+
return body
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [String]
|
66
|
+
def render_params()
|
67
|
+
return "()" if self.params.length == 0
|
68
|
+
# if self.params.length >= 5
|
69
|
+
# return "(\n" + self.params.map(&:render).join(",\n").indent(2) + "\n)"
|
70
|
+
# end
|
71
|
+
return "(" + self.params.map(&:render).join(", ") + ")"
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [String]
|
75
|
+
def render_return_type
|
76
|
+
type = self.return_type.nil? ? "void" : self.return_type
|
77
|
+
return "# @return [#{type}]"
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String]
|
81
|
+
def render_description()
|
82
|
+
return (self.description&.length&.positive? ? self.description.comment.strip : nil)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|