cobra_commander 0.4.0 → 0.7.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.
- checksums.yaml +4 -4
- data/.editorconfig +11 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +25 -0
- data/README.md +10 -6
- data/cobra_commander.gemspec +4 -2
- data/lib/cobra_commander.rb +23 -2
- data/lib/cobra_commander/affected.rb +37 -30
- data/lib/cobra_commander/cached_component_tree.rb +24 -0
- data/lib/cobra_commander/calculated_component_tree.rb +141 -0
- data/lib/cobra_commander/change.rb +6 -7
- data/lib/cobra_commander/cli.rb +61 -11
- data/lib/cobra_commander/component.rb +52 -0
- data/lib/cobra_commander/component_tree.rb +46 -117
- data/lib/cobra_commander/dependencies.rb +4 -0
- data/lib/cobra_commander/dependencies/bundler.rb +38 -0
- data/lib/cobra_commander/dependencies/yarn/package.rb +37 -0
- data/lib/cobra_commander/dependencies/yarn/package_repo.rb +31 -0
- data/lib/cobra_commander/dependencies/yarn_workspace.rb +55 -0
- data/lib/cobra_commander/executor.rb +20 -0
- data/lib/cobra_commander/graph.rb +6 -15
- data/lib/cobra_commander/output.rb +74 -0
- data/lib/cobra_commander/umbrella.rb +51 -0
- data/lib/cobra_commander/version.rb +1 -1
- data/renovate.json +8 -0
- metadata +29 -9
- data/lib/cobra_commander/formatted_output.rb +0 -76
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7155830962a165a9d34da676e053580b6a375543c0823cf89ce72db41b8fdc7e
|
4
|
+
data.tar.gz: db6fd674597394e4c5ee1f8e42b42d9e24305de678a9043f17ed026238f6edcc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee123f0d3b655a028e76e991fd3c7c0d411f9ece583965f9f4547a6356cce527049b41e90d665b18c433d5c8a293e43726c9a5b3fa822fb0a433d08b23df8f6e
|
7
|
+
data.tar.gz: 3f9514a4c74710643361ac3d3950e3f3d3b72e864b44c6f5addfc35997ae99a779bf07d5382cb38fa424a9d859de741d7836cd93e57624e55d044d8c0a0cfc0a
|
data/.editorconfig
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# EditorConfig is awesome: https://EditorConfig.org
|
2
|
+
# top-most EditorConfig file
|
3
|
+
root = true
|
4
|
+
|
5
|
+
# Unix-style newlines with a newline ending every file
|
6
|
+
[*]
|
7
|
+
end_of_line = lf
|
8
|
+
insert_final_newline = true
|
9
|
+
trim_trailing_whitespace = true
|
10
|
+
indent_style = space
|
11
|
+
indent_size = 2
|
data/.travis.yml
CHANGED
@@ -4,8 +4,9 @@ before_install:
|
|
4
4
|
- "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
|
5
5
|
- gem install bundler:"$BUNDLER_VERSION"
|
6
6
|
- sudo apt-get -qq install graphviz
|
7
|
+
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
|
8
|
+
- export PATH="$HOME/.yarn/bin:$PATH"
|
7
9
|
rvm:
|
8
10
|
- 2.5.1
|
9
11
|
env:
|
10
|
-
- BUNDLER_VERSION=1.
|
11
|
-
- BUNDLER_VERSION=1.16.3
|
12
|
+
- BUNDLER_VERSION=1.17.3
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## Version 0.7 - 2020-07-08
|
6
|
+
|
7
|
+
* Introduces CobraCommander::Umbrella with optimizations for dependency resolution
|
8
|
+
* Deprecates cobra cache and cache usages
|
9
|
+
|
10
|
+
## Version 0.6.1 - 2019-07-05
|
11
|
+
|
12
|
+
* Better supports yarn workspaces globbing by delegating to yarn to calculate the list of components rather than re-implementing in Ruby. PR [#34](https://github.com/powerhome/cobra_commander/pull/34)
|
13
|
+
|
14
|
+
## Version 0.6.0 - 2019-07-03
|
15
|
+
|
16
|
+
* Tracks package.json `workspaces` in addition to `dependencies` and `devDependencies`. PR [#31](https://github.com/powerhome/cobra_commander/pull/31)
|
17
|
+
* Permits caching the component tree of a project on disk to avoid calculating it repeatedly on subsequent cobra invocations.
|
18
|
+
|
19
|
+
## Version 0.5.1 - 2018-10-15
|
20
|
+
|
21
|
+
* Fix a bug with dependencies_of where it wouldn't match components further down the list of umbrella's dependencies
|
22
|
+
* Bumps Thor version from 0.19.4 to a range (< 2.0, >= 0.18.1) compatible with railties
|
23
|
+
|
24
|
+
## Version 0.5.0 - 2018-09-20
|
25
|
+
|
26
|
+
* Renames `dependencies_of` to `dependents_of`. PR [#25](https://github.com/powerhome/cobra_commander/pull/25)
|
27
|
+
* Add `dependencies_of` command list the direct and indirect dependencies of one component. PR [#25](https://github.com/powerhome/cobra_commander/pull/25)
|
28
|
+
* Add `do` command allow executing a command in the context of each component. PR [#26](https://github.com/powerhome/cobra_commander/pull/26)
|
29
|
+
|
5
30
|
## Version 0.4.0 - 2018-09-06
|
6
31
|
|
7
32
|
* Add `dependencies_of` command to permit listing or counting the dependencies of a particular component. PR [#24](https://github.com/powerhome/cobra_commander/pull/24)
|
data/README.md
CHANGED
@@ -28,16 +28,20 @@ Or install it yourself as:
|
|
28
28
|
|
29
29
|
```bash
|
30
30
|
Commands:
|
31
|
-
cobra
|
32
|
-
cobra
|
33
|
-
cobra
|
34
|
-
cobra
|
35
|
-
cobra
|
31
|
+
cobra cache APP_PATH CACHE_PATH # Caches a representation of the component structure of the app
|
32
|
+
cobra changes APP_PATH [--results=RESULTS] [--branch=BRANCH] [--cache=nil] # Prints list of changed files
|
33
|
+
cobra dependencies_of [component] [--app=pwd] [--format=FORMAT] [--cache=nil] # Outputs a list of components that [component] depends on within [app] context
|
34
|
+
cobra dependents_of [component] [--app=pwd] [--format=FORMAT] [--cache=nil] # Outputs count of components in [app] dependent on [component]
|
35
|
+
cobra do [command] [--app=pwd] [--cache=nil] # Executes the command in the context of each component in [app]
|
36
|
+
cobra graph APP_PATH [--format=FORMAT] [--cache=nil] # Outputs graph
|
37
|
+
cobra help [COMMAND] # Describe available commands or one specific command
|
38
|
+
cobra ls [app_path] [--app=pwd] [--format=FORMAT] [--cache=nil] # Prints tree of components for an app
|
39
|
+
cobra version # Prints version
|
36
40
|
```
|
37
41
|
|
38
42
|
## Development
|
39
43
|
|
40
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
44
|
+
After checking out the repo, run `bin/setup` to install dependencies. You will also need to install `graphviz` by running `brew install graphviz`. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
41
45
|
|
42
46
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
43
47
|
|
data/cobra_commander.gemspec
CHANGED
@@ -11,10 +11,12 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.authors = [
|
12
12
|
"Ben Langfeld",
|
13
13
|
"Garett Arrowood",
|
14
|
+
"Carlos Palhares",
|
14
15
|
]
|
15
16
|
spec.email = [
|
16
17
|
"blangfeld@powerhrg.com",
|
17
18
|
"garett.arrowood@powerhrg.com",
|
19
|
+
"carlos.palhares@powerhrg.com",
|
18
20
|
]
|
19
21
|
spec.summary = "Tools for working with Component Based Rails Apps"
|
20
22
|
spec.description = <<~DESCRIPTION
|
@@ -32,10 +34,10 @@ DESCRIPTION
|
|
32
34
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
33
35
|
spec.require_paths = ["lib"]
|
34
36
|
|
35
|
-
spec.add_dependency "thor", "
|
37
|
+
spec.add_dependency "thor", ["< 2.0", ">= 0.18.1"]
|
36
38
|
spec.add_dependency "ruby-graphviz", "~> 1.2.3"
|
37
39
|
|
38
|
-
spec.add_development_dependency "bundler", "~> 1.
|
40
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
39
41
|
spec.add_development_dependency "rake", "~> 10.0"
|
40
42
|
spec.add_development_dependency "rspec", "~> 3.5"
|
41
43
|
spec.add_development_dependency "guard-rspec"
|
data/lib/cobra_commander.rb
CHANGED
@@ -1,16 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "cobra_commander/dependencies"
|
4
|
+
require "cobra_commander/component"
|
5
|
+
require "cobra_commander/umbrella"
|
6
|
+
|
3
7
|
require "cobra_commander/cli"
|
4
|
-
require "cobra_commander/
|
8
|
+
require "cobra_commander/cached_component_tree"
|
9
|
+
require "cobra_commander/calculated_component_tree"
|
5
10
|
require "cobra_commander/version"
|
6
|
-
require "cobra_commander/formatted_output"
|
7
11
|
require "cobra_commander/graph"
|
8
12
|
require "cobra_commander/change"
|
9
13
|
require "cobra_commander/affected"
|
14
|
+
require "cobra_commander/output"
|
15
|
+
require "cobra_commander/executor"
|
10
16
|
|
11
17
|
# Tools for working with Component Based Rails Apps (see http://shageman.github.io/cbra.info/).
|
12
18
|
# Includes tools for graphing the components of an app and their relationships, as well as selectively
|
13
19
|
# testing components based on changes made.
|
14
20
|
module CobraCommander
|
15
21
|
UMBRELLA_APP_NAME = "App"
|
22
|
+
|
23
|
+
def self.umbrella(root_path, yarn: false, bundler: false, name: UMBRELLA_APP_NAME)
|
24
|
+
umbrella = Umbrella.new(name, root_path)
|
25
|
+
umbrella.add_source(:yarn, Dependencies::YarnWorkspace.new(root_path)) unless bundler
|
26
|
+
umbrella.add_source(:bundler, Dependencies::Bundler.new(root_path)) unless yarn
|
27
|
+
umbrella
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.umbrella_tree(path)
|
31
|
+
CalculatedComponentTree.new(UMBRELLA_APP_NAME, path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.tree_from_cache(cache_file)
|
35
|
+
CachedComponentTree.from_cache_file(cache_file)
|
36
|
+
end
|
16
37
|
end
|
@@ -3,28 +3,33 @@
|
|
3
3
|
module CobraCommander
|
4
4
|
# Calculates directly & transitively affected components
|
5
5
|
class Affected
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(tree, changes, path)
|
9
|
-
@tree = tree
|
6
|
+
def initialize(umbrella, changes)
|
7
|
+
@umbrella = umbrella
|
10
8
|
@changes = changes
|
11
|
-
@path = path
|
12
9
|
run!
|
13
10
|
end
|
14
11
|
|
15
12
|
def names
|
16
|
-
@names ||=
|
13
|
+
@names ||= all_affected.map(&:name)
|
17
14
|
end
|
18
15
|
|
19
16
|
def scripts
|
20
17
|
@scripts ||= paths.map { |path| File.join(path, "test.sh") }
|
21
18
|
end
|
22
19
|
|
20
|
+
def directly
|
21
|
+
@directly.map(&method(:affected_component))
|
22
|
+
end
|
23
|
+
|
24
|
+
def transitively
|
25
|
+
@transitively.map(&method(:affected_component))
|
26
|
+
end
|
27
|
+
|
23
28
|
def json_representation # rubocop:disable Metrics/MethodLength
|
24
29
|
{
|
25
30
|
changed_files: @changes,
|
26
|
-
directly_affected_components:
|
27
|
-
transitively_affected_components:
|
31
|
+
directly_affected_components: directly,
|
32
|
+
transitively_affected_components: transitively,
|
28
33
|
test_scripts: scripts,
|
29
34
|
component_names: names,
|
30
35
|
languages: {
|
@@ -39,46 +44,48 @@ module CobraCommander
|
|
39
44
|
def run!
|
40
45
|
@transitively = Set.new
|
41
46
|
@directly = Set.new
|
42
|
-
|
43
|
-
@transitively.
|
44
|
-
@
|
45
|
-
@directly = @directly.to_a.sort_by { |h| h[:name] }
|
47
|
+
@umbrella.components.each(&method(:add_if_changed))
|
48
|
+
@transitively = @transitively.sort_by(&:name)
|
49
|
+
@directly = @directly.sort_by(&:name)
|
46
50
|
end
|
47
51
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
52
|
+
def add_if_changed(component)
|
53
|
+
return if component.root_paths.uniq.none? { |path| @changes.any?(Regexp.new(path)) }
|
54
|
+
|
55
|
+
@directly << component
|
56
|
+
@transitively.merge(component.deep_dependents)
|
53
57
|
end
|
54
58
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
59
|
+
def affected_component(component)
|
60
|
+
{
|
61
|
+
name: component.name,
|
62
|
+
path: component.root_paths,
|
63
|
+
type: component.sources.keys.map(&:to_s).map(&:capitalize).join(" & "),
|
64
|
+
}
|
62
65
|
end
|
63
66
|
|
64
67
|
def all_affected
|
65
|
-
@all_affected ||= (@directly
|
68
|
+
@all_affected ||= (@directly | @transitively).sort_by(&:name)
|
66
69
|
end
|
67
70
|
|
68
71
|
def paths
|
69
|
-
@paths ||= all_affected.map
|
72
|
+
@paths ||= all_affected.map(&:root_paths).flatten.uniq
|
70
73
|
end
|
71
74
|
|
72
|
-
def
|
73
|
-
|
75
|
+
def all_affected_sources
|
76
|
+
all_affected
|
77
|
+
.map(&:sources)
|
78
|
+
.map(&:keys)
|
79
|
+
.flatten
|
80
|
+
.uniq
|
74
81
|
end
|
75
82
|
|
76
83
|
def contains_ruby?
|
77
|
-
|
84
|
+
all_affected_sources.include?(:bundler)
|
78
85
|
end
|
79
86
|
|
80
87
|
def contains_js?
|
81
|
-
|
88
|
+
all_affected_sources.include?(:yarn)
|
82
89
|
end
|
83
90
|
end
|
84
91
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cobra_commander/component_tree"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
# Represents a dependency tree in a given context, built from a cache
|
7
|
+
class CachedComponentTree < ComponentTree
|
8
|
+
attr_reader :dependencies
|
9
|
+
|
10
|
+
def self.from_cache_file(cache_file)
|
11
|
+
cache = JSON.parse(File.read(cache_file), symbolize_names: true)
|
12
|
+
new(cache)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(cache)
|
16
|
+
super(cache[:name], cache[:path])
|
17
|
+
@type = cache[:type]
|
18
|
+
@ancestry = Set.new(cache[:ancestry])
|
19
|
+
@dependencies = (cache[:dependencies] || []).map do |dep|
|
20
|
+
CachedComponentTree.new(dep)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cobra_commander/component_tree"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module CobraCommander
|
7
|
+
# Represents a dependency tree in a given context
|
8
|
+
class CalculatedComponentTree < ComponentTree
|
9
|
+
attr_reader :name, :path
|
10
|
+
|
11
|
+
def initialize(name, path, ancestry = Set.new)
|
12
|
+
super(name, path)
|
13
|
+
@ancestry = ancestry
|
14
|
+
@ruby = Ruby.new(path)
|
15
|
+
@js = Js.new(path)
|
16
|
+
@type = type_of_component
|
17
|
+
end
|
18
|
+
|
19
|
+
def dependencies
|
20
|
+
@deps ||= begin
|
21
|
+
deps = @ruby.dependencies + @js.dependencies
|
22
|
+
deps.sort_by { |dep| dep[:name] }
|
23
|
+
.map(&method(:dep_representation))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def type_of_component
|
30
|
+
return "Ruby & JS" if @ruby.gem? && @js.node?
|
31
|
+
return "Ruby" if @ruby.gem?
|
32
|
+
return "JS" if @js.node?
|
33
|
+
end
|
34
|
+
|
35
|
+
def dep_representation(dep)
|
36
|
+
full_path = File.expand_path(File.join(path, dep[:path]))
|
37
|
+
ancestry = @ancestry + [{ name: @name, path: path, type: @type }]
|
38
|
+
CalculatedComponentTree.new(dep[:name], full_path, ancestry)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Calculates ruby dependencies
|
42
|
+
class Ruby
|
43
|
+
def initialize(root_path)
|
44
|
+
@root_path = root_path
|
45
|
+
end
|
46
|
+
|
47
|
+
def dependencies
|
48
|
+
@deps ||= begin
|
49
|
+
return [] unless gem?
|
50
|
+
gems = bundler_definition.dependencies.select { |dep| path?(dep.source) }
|
51
|
+
format(gems)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def path?(source)
|
56
|
+
return if source.nil?
|
57
|
+
source_has_path = source.respond_to?(:path?) ? source.path? : source.is_a_path?
|
58
|
+
source_has_path && source.path.to_s != "."
|
59
|
+
end
|
60
|
+
|
61
|
+
def format(deps)
|
62
|
+
deps.map do |dep|
|
63
|
+
path = File.join(dep.source.path, dep.name)
|
64
|
+
{ name: dep.name, path: path }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def gem?
|
69
|
+
@gem ||= File.exist?(gemfile_path)
|
70
|
+
end
|
71
|
+
|
72
|
+
def bundler_definition
|
73
|
+
::Bundler::Definition.build(gemfile_path, gemfile_lock_path, nil)
|
74
|
+
end
|
75
|
+
|
76
|
+
def gemfile_path
|
77
|
+
File.join(@root_path, "Gemfile")
|
78
|
+
end
|
79
|
+
|
80
|
+
def gemfile_lock_path
|
81
|
+
File.join(@root_path, "Gemfile.lock")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Calculates js dependencies
|
86
|
+
class Js
|
87
|
+
def initialize(root_path)
|
88
|
+
@root_path = root_path
|
89
|
+
end
|
90
|
+
|
91
|
+
def dependencies
|
92
|
+
@deps ||= begin
|
93
|
+
return [] unless node?
|
94
|
+
json = JSON.parse(File.read(package_json_path))
|
95
|
+
combined_deps(json)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def format_dependencies(deps)
|
100
|
+
return [] if deps.nil?
|
101
|
+
linked_deps = deps.select { |_, v| v.start_with? "link:" }
|
102
|
+
linked_deps.map do |_, v|
|
103
|
+
relational_path = v.split("link:")[1]
|
104
|
+
dep_name = relational_path.split("/")[-1]
|
105
|
+
{ name: dep_name, path: relational_path }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def node?
|
110
|
+
@node ||= File.exist?(package_json_path)
|
111
|
+
end
|
112
|
+
|
113
|
+
def package_json_path
|
114
|
+
File.join(@root_path, "package.json")
|
115
|
+
end
|
116
|
+
|
117
|
+
def combined_deps(json)
|
118
|
+
worskpace_dependencies = build_workspaces(json["workspaces"])
|
119
|
+
dependencies = format_dependencies Hash(json["dependencies"]).merge(Hash(json["devDependencies"]))
|
120
|
+
(dependencies + worskpace_dependencies).uniq
|
121
|
+
end
|
122
|
+
|
123
|
+
def build_workspaces(workspaces)
|
124
|
+
return [] if workspaces.nil?
|
125
|
+
|
126
|
+
yarn_workspaces.map do |_, component|
|
127
|
+
{ name: component["location"].split("/")[-1], path: component["location"] }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def yarn_workspaces
|
134
|
+
@yarn_workspaces ||= begin
|
135
|
+
output, = Open3.capture2("yarn --json workspaces info", chdir: @root_path)
|
136
|
+
JSON.parse(JSON.parse(output)["data"])
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "cobra_commander/component_tree"
|
4
3
|
require "cobra_commander/affected"
|
5
4
|
require "open3"
|
6
5
|
|
@@ -9,12 +8,12 @@ module CobraCommander
|
|
9
8
|
class Change
|
10
9
|
InvalidSelectionError = Class.new(StandardError)
|
11
10
|
|
12
|
-
def initialize(
|
13
|
-
@root_dir = Dir.chdir(path) { `git rev-parse --show-toplevel`.chomp }
|
11
|
+
def initialize(umbrella, results, branch)
|
12
|
+
@root_dir = Dir.chdir(umbrella.path) { `git rev-parse --show-toplevel`.chomp }
|
14
13
|
@results = results
|
15
14
|
@branch = branch
|
16
|
-
@
|
17
|
-
@affected = Affected.new(@
|
15
|
+
@umbrella = umbrella
|
16
|
+
@affected = Affected.new(@umbrella, changes)
|
18
17
|
end
|
19
18
|
|
20
19
|
def run!
|
@@ -86,8 +85,8 @@ module CobraCommander
|
|
86
85
|
end
|
87
86
|
end
|
88
87
|
|
89
|
-
def display(
|
90
|
-
"#{
|
88
|
+
def display(name:, type:, **)
|
89
|
+
"#{name} - #{type}"
|
91
90
|
end
|
92
91
|
|
93
92
|
def blank_line
|