Skip to content

Commit 372d337

Browse files
joemsakvburzynski
andauthored
v1.1.0 - Extract plugin ceremony into automatic convenience methods on Team instances (#20)
* Reset registered plugins during bust_caches * Support all public instance methods on a given plugin * Move test setup helpers into shared support with proper mixin config * Add Utils.demodulize * Add TODO for overriding Plugin.root_key * Add root_key overriding with declaration method * Bump version 1.0.2 -> 1.1.0 * Rename root_key to data_accessor_name * Add integration for data_accessor_name override * Add backward-compatibility integration spec * refactor: use longform definition of modules over shorthand * Fix rubocop target version, io helpers, helper usage * Ruby 3.2 maintenance * Add tapioca and ignore vendor/bundle --------- Co-authored-by: Valerie Burzynski <valerie.burzynski@gusto.com>
1 parent 8867ee6 commit 372d337

File tree

14 files changed

+240
-46
lines changed

14 files changed

+240
-46
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/doc/
66
/pkg/
77
/spec/reports/
8+
/vendor/bundle
89
/tmp/
910

1011
# rspec failure tracking

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ Layout/MultilineMethodCallIndentation:
110110
Style/BlockDelimiters:
111111
Enabled: false
112112

113+
Style/StringLiterals:
114+
Enabled: false
115+
113116
# Sometimes we like methods like `get_packages`
114117
Naming/AccessorMethodName:
115118
Enabled: false

Gemfile.lock

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
PATH
22
remote: .
33
specs:
4-
code_teams (1.0.2)
4+
code_teams (1.1.0)
55
sorbet-runtime
66

77
GEM
88
remote: https://rubygems.org/
99
specs:
1010
ast (2.4.3)
11-
benchmark (0.4.0)
11+
benchmark (0.4.1)
1212
coderay (1.1.3)
1313
diff-lcs (1.6.2)
1414
erubi (1.13.1)
@@ -71,13 +71,14 @@ GEM
7171
lint_roller (~> 1.1)
7272
rubocop (~> 1.72, >= 1.72.1)
7373
ruby-progressbar (1.13.0)
74-
sorbet (0.5.12134)
75-
sorbet-static (= 0.5.12134)
76-
sorbet-runtime (0.5.12134)
77-
sorbet-static (0.5.12134-universal-darwin)
78-
sorbet-static-and-runtime (0.5.12134)
79-
sorbet (= 0.5.12134)
80-
sorbet-runtime (= 0.5.12134)
74+
sorbet (0.5.12142)
75+
sorbet-static (= 0.5.12142)
76+
sorbet-runtime (0.5.12142)
77+
sorbet-static (0.5.12142-universal-darwin)
78+
sorbet-static (0.5.12142-x86_64-linux)
79+
sorbet-static-and-runtime (0.5.12142)
80+
sorbet (= 0.5.12142)
81+
sorbet-runtime (= 0.5.12142)
8182
spoom (1.6.3)
8283
erubi (>= 1.10.0)
8384
prism (>= 0.28.0)

README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,43 @@ github:
5959
```
6060

6161
1) You can now use the following API to get GitHub information about that team:
62-
```ruby
63-
team = CodeTeams.find('My Team')
64-
MyGithubPlugin.for(team).github
65-
```
62+
63+
```ruby
64+
team = CodeTeams.find('My Team')
65+
members = team.github.members
66+
github_name = team.github.team
67+
```
68+
69+
Alternatively, you can assign an accessor method name that differs from the plugin's class name:
70+
71+
```ruby
72+
class MyPlugin < CodeTeams::Plugin
73+
data_accessor_name :other_name
74+
75+
def other_name
76+
# ...
77+
end
78+
end
79+
80+
# You can then use:
81+
team.other_name
82+
# similarly to the Github example above
83+
# You can then access data in the following manner:
84+
team.other_name.attribute_name
85+
```
86+
87+
However, to avoid confusion, it's recommended to use the naming convention
88+
whenever possible so that your accessor name matches your plugin's name
89+
6690
2) Running team validations (see below) will ensure all teams have a GitHub team specified
6791

68-
Your plugins can be as simple or as complex as you want. Here are some other things we use plugins for:
69-
- Identifying which teams own which feature flags
70-
- Mapping teams to specific portions of the code through `code_ownership`
71-
- Allowing teams to protect certain files and require approval on modification of certain files
72-
- Specifying owned dependencies (Ruby gems, JavaScript packages, and more)
73-
- Specifying how to get in touch with the team via Slack (their channel and handle)
92+
Your plugins can be as simple or as complex as you want. Here are some other things we use plugins for:
93+
94+
- Identifying which teams own which feature flags
95+
- Mapping teams to specific portions of the code through `code_ownership`
96+
- Allowing teams to protect certain files and require approval on modification of certain files
97+
- Specifying owned dependencies (Ruby gems, JavaScript packages, and more)
98+
- Specifying how to get in touch with the team via Slack (their channel and handle)
7499

75100
## Configuration
76101
You'll want to ensure that all teams are valid in your CI environment. We recommend running code like this in CI:

bin/rspec

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
#
5+
# This file was generated by Bundler.
6+
#
7+
# The application 'rspec' is installed as part of a gem, and
8+
# this file is here to facilitate running it.
9+
#
10+
11+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12+
13+
bundle_binstub = File.expand_path("bundle", __dir__)
14+
15+
if File.file?(bundle_binstub)
16+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17+
load(bundle_binstub)
18+
else
19+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21+
end
22+
end
23+
24+
require "rubygems"
25+
require "bundler/setup"
26+
27+
load Gem.bin_path("rspec-core", "rspec")

code_teams.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |spec|
22
spec.name = 'code_teams'
3-
spec.version = '1.0.2'
3+
spec.version = '1.1.0'
44
spec.authors = ['Gusto Engineers']
55
spec.email = ['dev@gusto.com']
66
spec.summary = 'A low-dependency gem for declaring and querying engineering teams'

lib/code_teams.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
require 'sorbet-runtime'
88
require 'code_teams/plugin'
99
require 'code_teams/plugins/identity'
10+
require 'code_teams/utils'
1011

1112
module CodeTeams
1213
extend T::Sig
1314

1415
class IncorrectPublicApiUsageError < StandardError; end
1516

1617
UNKNOWN_TEAM_STRING = 'Unknown Team'
18+
@plugins_registered = T.let(false, T::Boolean)
1719

1820
sig { returns(T::Array[Team]) }
1921
def self.all
@@ -35,6 +37,11 @@ def self.find(name)
3537

3638
sig { params(dir: String).returns(T::Array[Team]) }
3739
def self.for_directory(dir)
40+
unless @plugins_registered
41+
Team.register_plugins
42+
@plugins_registered = true
43+
end
44+
3845
Pathname.new(dir).glob('**/*.yml').map do |path|
3946
Team.from_yml(path.to_s)
4047
rescue Psych::SyntaxError
@@ -59,6 +66,7 @@ def self.tag_value_for(string)
5966
# The primary reason this is helpful is for clients of CodeTeams who want to test their code, and each test context has different set of teams
6067
sig { void }
6168
def self.bust_caches!
69+
@plugins_registered = false
6270
Plugin.bust_caches!
6371
@all = nil
6472
@index_by_name = nil
@@ -85,6 +93,17 @@ def self.from_hash(raw_hash)
8593
)
8694
end
8795

96+
sig { void }
97+
def self.register_plugins
98+
Plugin.all_plugins.each do |plugin|
99+
# e.g., def github (on Team)
100+
define_method(plugin.data_accessor_name) do
101+
# e.g., MyGithubPlugin.for(team).github
102+
plugin.for(T.cast(self, Team)).public_send(plugin.data_accessor_name)
103+
end
104+
end
105+
end
106+
88107
sig { returns(T::Hash[T.untyped, T.untyped]) }
89108
attr_reader :raw_hash
90109

lib/code_teams/plugin.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,24 @@ class Plugin
1010

1111
abstract!
1212

13+
@data_accessor_name = T.let(nil, T.nilable(String))
14+
1315
sig { params(team: Team).void }
1416
def initialize(team)
1517
@team = team
1618
end
1719

20+
sig { params(key: String).returns(String) }
21+
def self.data_accessor_name(key = default_data_accessor_name)
22+
@data_accessor_name ||= key
23+
end
24+
25+
sig { returns(String) }
26+
def self.default_data_accessor_name
27+
# e.g., MyNamespace::MyPlugin -> my_plugin
28+
Utils.underscore(Utils.demodulize(name))
29+
end
30+
1831
sig { params(base: T.untyped).void }
1932
def self.inherited(base) # rubocop:disable Lint/MissingSuper
2033
all_plugins << T.cast(base, T.class_of(Plugin))

lib/code_teams/utils.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module CodeTeams
2+
module Utils
3+
module_function
4+
5+
def underscore(string)
6+
string.gsub('::', '/')
7+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
8+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
9+
.tr('-', '_')
10+
.downcase
11+
end
12+
13+
def demodulize(string)
14+
string.split('::').last
15+
end
16+
end
17+
end

sorbet/rbi/todo.rbi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
# typed: strong
55
module ::RSpec; end
66
module ::TestPlugin; end
7+
module TestNamespace::TestPlugin; end

0 commit comments

Comments
 (0)