Skip to content

Commit

Permalink
Allow mappers and validators to be injected by the client (#38)
Browse files Browse the repository at this point in the history
* Allow mappers and validators to be injected into CodeOwnership

* Bump version
  • Loading branch information
Alex Evanczuk authored Mar 8, 2023
1 parent 542a207 commit 80d85b4
Show file tree
Hide file tree
Showing 24 changed files with 325 additions and 198 deletions.
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
code_ownership (1.31.1)
code_ownership (1.32.0)
code_teams (~> 1.0)
packs
sorbet-runtime
Expand All @@ -10,7 +10,7 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
code_teams (1.0.0)
code_teams (1.0.1)
sorbet-runtime
coderay (1.1.3)
diff-lcs (1.4.4)
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ js_package_paths:

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

### Custom Ownership
To enable custom ownership, you can inject your own custom classes into `code_ownership`.
To do this, first create a class that adheres to the `CodeOwnership::Mapper` and/or `CodeOwnership::Validator` interface.
Then, in `config/code_ownership.yml`, you can require that file:
```yml
require:
- ./lib/my_extension.rb
```

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.

## Usage: Reading CodeOwnership
### `for_file`
`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.
Expand Down
2 changes: 1 addition & 1 deletion code_ownership.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = "code_ownership"
spec.version = '1.31.1'
spec.version = '1.32.0'
spec.authors = ['Gusto Engineers']
spec.email = ['[email protected]']
spec.summary = 'A gem to help engineering teams declare ownership of code'
Expand Down
16 changes: 12 additions & 4 deletions lib/code_ownership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
require 'sorbet-runtime'
require 'json'
require 'packs'
require 'code_ownership/cli'
require 'code_ownership/mapper'
require 'code_ownership/validator'
require 'code_ownership/private'
require 'code_ownership/cli'
require 'code_ownership/configuration'

module CodeOwnership
extend self
Expand All @@ -27,7 +30,7 @@ def for_file(file)

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

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

ownership_information << "# Code Ownership Report for `#{team.name}` Team"
Private::OwnershipMappers::Interface.all.each do |mapper|
Mapper.all.each do |mapper|
ownership_information << "## #{mapper.description}"
codeowners_lines = mapper.codeowners_lines_to_owners
ownership_for_mapper = []
Expand Down Expand Up @@ -172,6 +175,11 @@ def self.bust_caches!
@for_file = nil
@memoized_values = nil
Private.bust_caches!
Private::OwnershipMappers::Interface.all.each(&:bust_caches!)
Mapper.all.each(&:bust_caches!)
end

sig { returns(Configuration) }
def self.configuration
Private.configuration
end
end
44 changes: 44 additions & 0 deletions lib/code_ownership/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# typed: strict

module CodeOwnership
class Configuration < T::Struct
extend T::Sig
DEFAULT_JS_PACKAGE_PATHS = T.let(['**/'], T::Array[String])

const :owned_globs, T::Array[String]
const :unowned_globs, T::Array[String]
const :js_package_paths, T::Array[String]
const :unbuilt_gems_path, T.nilable(String)
const :skip_codeowners_validation, T::Boolean
const :raw_hash, T::Hash[T.untyped, T.untyped]

sig { returns(Configuration) }
def self.fetch
config_hash = YAML.load_file('config/code_ownership.yml')

if config_hash.key?("require")
config_hash["require"].each do |require_directive|
Private::ExtensionLoader.load(require_directive)
end
end

new(
owned_globs: config_hash.fetch('owned_globs', []),
unowned_globs: config_hash.fetch('unowned_globs', []),
js_package_paths: js_package_paths(config_hash),
skip_codeowners_validation: config_hash.fetch('skip_codeowners_validation', false),
raw_hash: config_hash
)
end

sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[String]) }
def self.js_package_paths(config_hash)
specified_package_paths = config_hash['js_package_paths']
if specified_package_paths.nil?
DEFAULT_JS_PACKAGE_PATHS.dup
else
Array(specified_package_paths)
end
end
end
end
62 changes: 62 additions & 0 deletions lib/code_ownership/mapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

# typed: strict

module CodeOwnership
module Mapper
extend T::Sig
extend T::Helpers

interface!

class << self
extend T::Sig

sig { params(base: Class).void }
def included(base)
@mappers ||= T.let(@mappers, T.nilable(T::Array[Class]))
@mappers ||= []
@mappers << base
end

sig { returns(T::Array[Mapper]) }
def all
T.unsafe(@mappers).map(&:new)
end
end

#
# This should be fast when run with ONE file
#
sig do
abstract.params(file: String).
returns(T.nilable(::CodeTeams::Team))
end
def map_file_to_owner(file)
end

#
# This should be fast when run with MANY files
#
sig do
abstract.params(files: T::Array[String]).
returns(T::Hash[String, T.nilable(::CodeTeams::Team)])
end
def map_files_to_owners(files)
end

sig do
abstract.returns(T::Hash[String, T.nilable(::CodeTeams::Team)])
end
def codeowners_lines_to_owners
end

sig { abstract.returns(String) }
def description
end

sig { abstract.void }
def bust_caches!
end
end
end
14 changes: 6 additions & 8 deletions lib/code_ownership/private.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

# typed: strict

require 'code_ownership/private/configuration'
require 'code_ownership/private/extension_loader'
require 'code_ownership/private/team_plugins/ownership'
require 'code_ownership/private/team_plugins/github'
require 'code_ownership/private/parse_js_packages'
require 'code_ownership/private/validations/interface'
require 'code_ownership/private/validations/files_have_owners'
require 'code_ownership/private/validations/github_codeowners_up_to_date'
require 'code_ownership/private/validations/files_have_unique_owners'
require 'code_ownership/private/ownership_mappers/interface'
require 'code_ownership/private/ownership_mappers/file_annotations'
require 'code_ownership/private/ownership_mappers/team_globs'
require 'code_ownership/private/ownership_mappers/package_ownership'
Expand All @@ -21,10 +19,10 @@ module CodeOwnership
module Private
extend T::Sig

sig { returns(Private::Configuration) }
sig { returns(Configuration) }
def self.configuration
@configuration ||= T.let(@configuration, T.nilable(Private::Configuration))
@configuration ||= Private::Configuration.fetch
@configuration ||= T.let(@configuration, T.nilable(Configuration))
@configuration ||= Configuration.fetch
end

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

sig { params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).void }
def self.validate!(files:, autocorrect: true, stage_changes: true)
errors = Validations::Interface.all.flat_map do |validator|
errors = Validator.all.flat_map do |validator|
validator.validation_errors(
files: files,
autocorrect: autocorrect,
Expand Down Expand Up @@ -87,7 +85,7 @@ def self.files_by_mapper(files)
@files_by_mapper ||= begin
files_by_mapper = files.map { |file| [file, []] }.to_h

Private::OwnershipMappers::Interface.all.each do |mapper|
Mapper.all.each do |mapper|
mapper.map_files_to_owners(files).each do |file, _team|
files_by_mapper[file] ||= []
T.must(files_by_mapper[file]) << mapper.description
Expand Down
37 changes: 0 additions & 37 deletions lib/code_ownership/private/configuration.rb

This file was deleted.

24 changes: 24 additions & 0 deletions lib/code_ownership/private/extension_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# typed: strict
# frozen_string_literal: true

module CodeOwnership
module Private
# This class handles loading extensions to code_ownership using the `require` directive
# in the `code_ownership.yml` configuration.
module ExtensionLoader
class << self
extend T::Sig
sig { params(require_directive: String).void }
def load(require_directive)
# We want to transform the require directive to behave differently
# if it's a specific local file being required versus a gem
if require_directive.start_with?(".")
require File.join(Pathname.pwd, require_directive)
else
require require_directive
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module OwnershipMappers
# }
class FileAnnotations
extend T::Sig
include Interface
include Mapper

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

Expand Down
66 changes: 0 additions & 66 deletions lib/code_ownership/private/ownership_mappers/interface.rb

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Private
module OwnershipMappers
class JsPackageOwnership
extend T::Sig
include Interface
include Mapper

@@package_json_cache = T.let({}, T::Hash[String, T.nilable(ParseJsPackages::Package)]) # rubocop:disable Style/ClassVars

Expand Down
Loading

0 comments on commit 80d85b4

Please sign in to comment.