Skip to content

Commit 80d85b4

Browse files
author
Alex Evanczuk
authored
Allow mappers and validators to be injected by the client (#38)
* Allow mappers and validators to be injected into CodeOwnership * Bump version
1 parent 542a207 commit 80d85b4

24 files changed

+325
-198
lines changed

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
code_ownership (1.31.1)
4+
code_ownership (1.32.0)
55
code_teams (~> 1.0)
66
packs
77
sorbet-runtime
@@ -10,7 +10,7 @@ GEM
1010
remote: https://rubygems.org/
1111
specs:
1212
ast (2.4.2)
13-
code_teams (1.0.0)
13+
code_teams (1.0.1)
1414
sorbet-runtime
1515
coderay (1.1.3)
1616
diff-lcs (1.4.4)

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ js_package_paths:
5757

5858
This defaults `**/`, which makes it look for `package.json` files across your application.
5959

60+
### Custom Ownership
61+
To enable custom ownership, you can inject your own custom classes into `code_ownership`.
62+
To do this, first create a class that adheres to the `CodeOwnership::Mapper` and/or `CodeOwnership::Validator` interface.
63+
Then, in `config/code_ownership.yml`, you can require that file:
64+
```yml
65+
require:
66+
- ./lib/my_extension.rb
67+
```
68+
69+
Now, `bin/codeownership validate` will automatically include your new mapper and/or validator. See [`spec/lib/code_ownership/private/extension_loader_spec.rb](spec/lib/code_ownership/private/extension_loader_spec.rb) for an example of what this looks like.
70+
6071
## Usage: Reading CodeOwnership
6172
### `for_file`
6273
`CodeOwnership.for_file`, given a relative path to a file returns a `CodeTeams::Team` if there is a team that owns the file, `nil` otherwise.

code_ownership.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_ownership"
3-
spec.version = '1.31.1'
3+
spec.version = '1.32.0'
44
spec.authors = ['Gusto Engineers']
55
spec.email = ['[email protected]']
66
spec.summary = 'A gem to help engineering teams declare ownership of code'

lib/code_ownership.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
require 'sorbet-runtime'
88
require 'json'
99
require 'packs'
10-
require 'code_ownership/cli'
10+
require 'code_ownership/mapper'
11+
require 'code_ownership/validator'
1112
require 'code_ownership/private'
13+
require 'code_ownership/cli'
14+
require 'code_ownership/configuration'
1215

1316
module CodeOwnership
1417
extend self
@@ -27,7 +30,7 @@ def for_file(file)
2730

2831
owner = T.let(nil, T.nilable(CodeTeams::Team))
2932

30-
Private::OwnershipMappers::Interface.all.each do |mapper|
33+
Mapper.all.each do |mapper|
3134
owner = mapper.map_file_to_owner(file)
3235
break if owner
3336
end
@@ -41,7 +44,7 @@ def for_team(team)
4144
ownership_information = T.let([], T::Array[String])
4245

4346
ownership_information << "# Code Ownership Report for `#{team.name}` Team"
44-
Private::OwnershipMappers::Interface.all.each do |mapper|
47+
Mapper.all.each do |mapper|
4548
ownership_information << "## #{mapper.description}"
4649
codeowners_lines = mapper.codeowners_lines_to_owners
4750
ownership_for_mapper = []
@@ -172,6 +175,11 @@ def self.bust_caches!
172175
@for_file = nil
173176
@memoized_values = nil
174177
Private.bust_caches!
175-
Private::OwnershipMappers::Interface.all.each(&:bust_caches!)
178+
Mapper.all.each(&:bust_caches!)
179+
end
180+
181+
sig { returns(Configuration) }
182+
def self.configuration
183+
Private.configuration
176184
end
177185
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# typed: strict
2+
3+
module CodeOwnership
4+
class Configuration < T::Struct
5+
extend T::Sig
6+
DEFAULT_JS_PACKAGE_PATHS = T.let(['**/'], T::Array[String])
7+
8+
const :owned_globs, T::Array[String]
9+
const :unowned_globs, T::Array[String]
10+
const :js_package_paths, T::Array[String]
11+
const :unbuilt_gems_path, T.nilable(String)
12+
const :skip_codeowners_validation, T::Boolean
13+
const :raw_hash, T::Hash[T.untyped, T.untyped]
14+
15+
sig { returns(Configuration) }
16+
def self.fetch
17+
config_hash = YAML.load_file('config/code_ownership.yml')
18+
19+
if config_hash.key?("require")
20+
config_hash["require"].each do |require_directive|
21+
Private::ExtensionLoader.load(require_directive)
22+
end
23+
end
24+
25+
new(
26+
owned_globs: config_hash.fetch('owned_globs', []),
27+
unowned_globs: config_hash.fetch('unowned_globs', []),
28+
js_package_paths: js_package_paths(config_hash),
29+
skip_codeowners_validation: config_hash.fetch('skip_codeowners_validation', false),
30+
raw_hash: config_hash
31+
)
32+
end
33+
34+
sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[String]) }
35+
def self.js_package_paths(config_hash)
36+
specified_package_paths = config_hash['js_package_paths']
37+
if specified_package_paths.nil?
38+
DEFAULT_JS_PACKAGE_PATHS.dup
39+
else
40+
Array(specified_package_paths)
41+
end
42+
end
43+
end
44+
end

lib/code_ownership/mapper.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
# typed: strict
4+
5+
module CodeOwnership
6+
module Mapper
7+
extend T::Sig
8+
extend T::Helpers
9+
10+
interface!
11+
12+
class << self
13+
extend T::Sig
14+
15+
sig { params(base: Class).void }
16+
def included(base)
17+
@mappers ||= T.let(@mappers, T.nilable(T::Array[Class]))
18+
@mappers ||= []
19+
@mappers << base
20+
end
21+
22+
sig { returns(T::Array[Mapper]) }
23+
def all
24+
T.unsafe(@mappers).map(&:new)
25+
end
26+
end
27+
28+
#
29+
# This should be fast when run with ONE file
30+
#
31+
sig do
32+
abstract.params(file: String).
33+
returns(T.nilable(::CodeTeams::Team))
34+
end
35+
def map_file_to_owner(file)
36+
end
37+
38+
#
39+
# This should be fast when run with MANY files
40+
#
41+
sig do
42+
abstract.params(files: T::Array[String]).
43+
returns(T::Hash[String, T.nilable(::CodeTeams::Team)])
44+
end
45+
def map_files_to_owners(files)
46+
end
47+
48+
sig do
49+
abstract.returns(T::Hash[String, T.nilable(::CodeTeams::Team)])
50+
end
51+
def codeowners_lines_to_owners
52+
end
53+
54+
sig { abstract.returns(String) }
55+
def description
56+
end
57+
58+
sig { abstract.void }
59+
def bust_caches!
60+
end
61+
end
62+
end

lib/code_ownership/private.rb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
# typed: strict
44

5-
require 'code_ownership/private/configuration'
5+
require 'code_ownership/private/extension_loader'
66
require 'code_ownership/private/team_plugins/ownership'
77
require 'code_ownership/private/team_plugins/github'
88
require 'code_ownership/private/parse_js_packages'
9-
require 'code_ownership/private/validations/interface'
109
require 'code_ownership/private/validations/files_have_owners'
1110
require 'code_ownership/private/validations/github_codeowners_up_to_date'
1211
require 'code_ownership/private/validations/files_have_unique_owners'
13-
require 'code_ownership/private/ownership_mappers/interface'
1412
require 'code_ownership/private/ownership_mappers/file_annotations'
1513
require 'code_ownership/private/ownership_mappers/team_globs'
1614
require 'code_ownership/private/ownership_mappers/package_ownership'
@@ -21,10 +19,10 @@ module CodeOwnership
2119
module Private
2220
extend T::Sig
2321

24-
sig { returns(Private::Configuration) }
22+
sig { returns(Configuration) }
2523
def self.configuration
26-
@configuration ||= T.let(@configuration, T.nilable(Private::Configuration))
27-
@configuration ||= Private::Configuration.fetch
24+
@configuration ||= T.let(@configuration, T.nilable(Configuration))
25+
@configuration ||= Configuration.fetch
2826
end
2927

3028
sig { void }
@@ -36,7 +34,7 @@ def self.bust_caches!
3634

3735
sig { params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).void }
3836
def self.validate!(files:, autocorrect: true, stage_changes: true)
39-
errors = Validations::Interface.all.flat_map do |validator|
37+
errors = Validator.all.flat_map do |validator|
4038
validator.validation_errors(
4139
files: files,
4240
autocorrect: autocorrect,
@@ -87,7 +85,7 @@ def self.files_by_mapper(files)
8785
@files_by_mapper ||= begin
8886
files_by_mapper = files.map { |file| [file, []] }.to_h
8987

90-
Private::OwnershipMappers::Interface.all.each do |mapper|
88+
Mapper.all.each do |mapper|
9189
mapper.map_files_to_owners(files).each do |file, _team|
9290
files_by_mapper[file] ||= []
9391
T.must(files_by_mapper[file]) << mapper.description

lib/code_ownership/private/configuration.rb

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module CodeOwnership
5+
module Private
6+
# This class handles loading extensions to code_ownership using the `require` directive
7+
# in the `code_ownership.yml` configuration.
8+
module ExtensionLoader
9+
class << self
10+
extend T::Sig
11+
sig { params(require_directive: String).void }
12+
def load(require_directive)
13+
# We want to transform the require directive to behave differently
14+
# if it's a specific local file being required versus a gem
15+
if require_directive.start_with?(".")
16+
require File.join(Pathname.pwd, require_directive)
17+
else
18+
require require_directive
19+
end
20+
end
21+
end
22+
end
23+
end
24+
end

lib/code_ownership/private/ownership_mappers/file_annotations.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module OwnershipMappers
1616
# }
1717
class FileAnnotations
1818
extend T::Sig
19-
include Interface
19+
include Mapper
2020

2121
@@map_files_to_owners = T.let({}, T.nilable(T::Hash[String, T.nilable(::CodeTeams::Team)])) # rubocop:disable Style/ClassVars
2222

0 commit comments

Comments
 (0)