code_teams 1.1.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 755f17a56df0e3c1f337e0497b4b63887b0681626583bdf3a6f66ec4eb9ce46f
4
- data.tar.gz: e060b12412a3a5c0ff02af18490a3ef336b8b83d5236ef5eed0f4a75c12ced2b
3
+ metadata.gz: c4e5cbd18c2a3348571f8b1a2c4cb556aff50b4e87322fefce0b50406fee8818
4
+ data.tar.gz: 3e8d66d171ce6a98b5eb9371b2edc96ad1f4ad6d9a75c5b324774ad173168363
5
5
  SHA512:
6
- metadata.gz: 92594da3e4eccae671cd71d1564daf10009d41bd30612a0d5c1b66e3a729466d2c03ccea4c5cad5cf2196bc85be33a06520ec4767e94bb75219f070c8088449f
7
- data.tar.gz: 9acbb9c011703218fe398179e723c796452b4564a06817124fd7cd599ea9e93a6d7f01987e0f03f274ddd100074d5d8260a18a85d51da69ae15a458bd992e29b
6
+ metadata.gz: c28e11c45ab2d51b0dedf49f5ff1b33591ab68904a7e7728e74bb7bfa44bca2a6b5795195f95be1c37728cab5b4ca0f7faa6d0827f64866987bb79c682e31ab0
7
+ data.tar.gz: c98171075d5059ad935e00bef6c4d0e79a391f7329e078677732b53f0465511a8e862399845b3c062e446b40b2ae123bc1f06c64c3950bcaa9b72fb91293445d
data/README.md CHANGED
@@ -111,6 +111,32 @@ if errors.any?
111
111
  end
112
112
  ```
113
113
 
114
+ ## Testing
115
+
116
+ `code_teams` provides test helpers for creating temporary teams in your specs without writing YML files to disk. Add the following to your `spec_helper.rb` (or `rails_helper.rb`):
117
+
118
+ ```ruby
119
+ require 'code_teams/testing'
120
+
121
+ CodeTeams::Testing.enable!
122
+ ```
123
+
124
+ This gives you:
125
+ - A `code_team_with_config` helper method available in all specs
126
+ - Automatic cleanup of testing teams between examples
127
+
128
+ Example usage in a spec:
129
+
130
+ ```ruby
131
+ RSpec.describe 'my feature' do
132
+ it 'works with a team' do
133
+ team = code_team_with_config(name: 'Test Team')
134
+
135
+ expect(CodeTeams.find('Test Team')).to eq(team)
136
+ end
137
+ end
138
+ ```
139
+
114
140
  ## Contributing
115
141
 
116
142
  Bug reports and pull requests are welcome!
@@ -25,12 +25,12 @@ module CodeTeams
25
25
  sig { returns(String) }
26
26
  def self.default_data_accessor_name
27
27
  # e.g., MyNamespace::MyPlugin -> my_plugin
28
- Utils.underscore(Utils.demodulize(name))
28
+ Utils.underscore(Utils.demodulize(T.must(name)))
29
29
  end
30
30
 
31
- sig { params(base: T.untyped).void }
31
+ sig { params(base: T.class_of(Plugin)).void }
32
32
  def self.inherited(base) # rubocop:disable Lint/MissingSuper
33
- all_plugins << T.cast(base, T.class_of(Plugin))
33
+ all_plugins << base
34
34
  end
35
35
 
36
36
  sig { returns(T::Array[T.class_of(Plugin)]) }
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # typed: false
4
+
5
+ require 'securerandom'
6
+ require 'code_teams/testing'
7
+
8
+ module CodeTeams
9
+ module Testing
10
+ module RSpecHelpers
11
+ def code_team_with_config(team_config = {})
12
+ team_config = team_config.dup
13
+ team_config[:name] ||= "Fake Team #{SecureRandom.hex(4)}"
14
+ CodeTeams::Testing.create_code_team(team_config)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # typed: strict
4
+
5
+ require 'securerandom'
6
+ require 'code_teams'
7
+ require 'code_teams/testing/rspec_helpers'
8
+
9
+ module CodeTeams
10
+ # Utilities for tests that need a controlled set of teams without writing YML
11
+ # files to disk.
12
+ #
13
+ # Opt-in by requiring `code_teams/testing`.
14
+ module Testing
15
+ extend T::Sig
16
+
17
+ THREAD_KEY = T.let(:__code_teams_collection, Symbol)
18
+ @enabled = T.let(false, T::Boolean)
19
+
20
+ sig { void }
21
+ def self.enable!
22
+ return if @enabled
23
+
24
+ CodeTeams.prepend(CodeTeamsExtension)
25
+ @enabled = true
26
+
27
+ return unless defined?(RSpec)
28
+
29
+ T.unsafe(RSpec).configure do |config|
30
+ config.include CodeTeams::Testing::RSpecHelpers
31
+
32
+ config.around do |example|
33
+ example.run
34
+ # Bust caches because plugins may hang onto stale data between examples.
35
+ if CodeTeams::Testing.code_teams.any?
36
+ CodeTeams.bust_caches!
37
+ CodeTeams::Testing.reset!
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(CodeTeams::Team) }
44
+ def self.create_code_team(attributes)
45
+ attributes = attributes.dup
46
+ attributes[:name] ||= "Fake Team #{SecureRandom.hex(4)}"
47
+
48
+ code_team = CodeTeams::Team.new(
49
+ config_yml: 'tmp/fake_config.yml',
50
+ raw_hash: Utils.deep_stringify_keys(attributes)
51
+ )
52
+
53
+ code_teams << code_team
54
+ code_team
55
+ end
56
+
57
+ sig { returns(T::Array[CodeTeams::Team]) }
58
+ def self.code_teams
59
+ existing = Thread.current[THREAD_KEY]
60
+ return existing if existing.is_a?(Array)
61
+
62
+ Thread.current[THREAD_KEY] = []
63
+ T.cast(Thread.current[THREAD_KEY], T::Array[CodeTeams::Team])
64
+ end
65
+
66
+ sig { void }
67
+ def self.reset!
68
+ Thread.current[THREAD_KEY] = []
69
+ end
70
+
71
+ module CodeTeamsExtension
72
+ extend T::Sig
73
+
74
+ sig { params(base: Module).void }
75
+ def self.prepended(base)
76
+ base.singleton_class.prepend(ClassMethods)
77
+ end
78
+
79
+ module ClassMethods
80
+ extend T::Sig
81
+
82
+ sig { returns(T::Array[CodeTeams::Team]) }
83
+ def all
84
+ CodeTeams::Testing.code_teams + super
85
+ end
86
+
87
+ sig { params(name: String).returns(T.nilable(CodeTeams::Team)) }
88
+ def find(name)
89
+ CodeTeams::Testing.code_teams.find { |t| t.name == name } || super
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # typed: strict
4
+
1
5
  module CodeTeams
2
6
  module Utils
7
+ extend T::Sig
8
+
3
9
  module_function
4
10
 
11
+ sig { params(string: String).returns(String) }
5
12
  def underscore(string)
6
13
  string.gsub('::', '/')
7
14
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -10,8 +17,24 @@ module CodeTeams
10
17
  .downcase
11
18
  end
12
19
 
20
+ sig { params(string: String).returns(String) }
13
21
  def demodulize(string)
14
- string.split('::').last
22
+ T.must(string.split('::').last)
23
+ end
24
+
25
+ # Recursively converts symbol keys to strings. Top-level input should be a Hash.
26
+ sig { params(value: T.untyped).returns(T.untyped) }
27
+ def deep_stringify_keys(value)
28
+ case value
29
+ when Hash
30
+ value.each_with_object({}) do |(k, v), acc|
31
+ acc[k.to_s] = deep_stringify_keys(v)
32
+ end
33
+ when Array
34
+ value.map { |v| deep_stringify_keys(v) }
35
+ else
36
+ value
37
+ end
15
38
  end
16
39
  end
17
40
  end
data/lib/code_teams.rb CHANGED
@@ -13,28 +13,27 @@ module CodeTeams
13
13
  extend T::Sig
14
14
 
15
15
  class IncorrectPublicApiUsageError < StandardError; end
16
+ class TeamNotFoundError < StandardError; end
16
17
 
17
18
  UNKNOWN_TEAM_STRING = 'Unknown Team'
18
19
  @plugins_registered = T.let(false, T::Boolean)
19
20
 
20
21
  sig { returns(T::Array[Team]) }
21
22
  def self.all
22
- @all = T.let(@all, T.nilable(T::Array[Team]))
23
- @all ||= for_directory('config/teams')
23
+ @all ||= T.let(for_directory('config/teams'), T.nilable(T::Array[Team]))
24
24
  end
25
25
 
26
26
  sig { params(name: String).returns(T.nilable(Team)) }
27
27
  def self.find(name)
28
- @index_by_name = T.let(@index_by_name, T.nilable(T::Hash[String, CodeTeams::Team]))
29
- @index_by_name ||= begin
30
- result = {}
31
- all.each { |t| result[t.name] = t }
32
- result
33
- end
34
-
28
+ @index_by_name ||= T.let(all.to_h { |t| [t.name, t] }, T.nilable(T::Hash[String, CodeTeams::Team]))
35
29
  @index_by_name[name]
36
30
  end
37
31
 
32
+ sig { params(name: String).returns(Team) }
33
+ def self.find!(name)
34
+ find(name) || raise(TeamNotFoundError, "No team found with name: #{name}")
35
+ end
36
+
38
37
  sig { params(dir: String).returns(T::Array[Team]) }
39
38
  def self.for_directory(dir)
40
39
  unless @plugins_registered
@@ -85,7 +84,7 @@ module CodeTeams
85
84
  )
86
85
  end
87
86
 
88
- sig { params(raw_hash: T::Hash[T.untyped, T.untyped]).returns(Team) }
87
+ sig { params(raw_hash: T::Hash[String, T.untyped]).returns(Team) }
89
88
  def self.from_hash(raw_hash)
90
89
  new(
91
90
  config_yml: nil,
@@ -104,7 +103,7 @@ module CodeTeams
104
103
  end
105
104
  end
106
105
 
107
- sig { returns(T::Hash[T.untyped, T.untyped]) }
106
+ sig { returns(T::Hash[String, T.untyped]) }
108
107
  attr_reader :raw_hash
109
108
 
110
109
  sig { returns(T.nilable(String)) }
@@ -113,7 +112,7 @@ module CodeTeams
113
112
  sig do
114
113
  params(
115
114
  config_yml: T.nilable(String),
116
- raw_hash: T::Hash[T.untyped, T.untyped]
115
+ raw_hash: T::Hash[String, T.untyped]
117
116
  ).void
118
117
  end
119
118
  def initialize(config_yml:, raw_hash:)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code_teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
@@ -34,6 +34,8 @@ files:
34
34
  - lib/code_teams.rb
35
35
  - lib/code_teams/plugin.rb
36
36
  - lib/code_teams/plugins/identity.rb
37
+ - lib/code_teams/testing.rb
38
+ - lib/code_teams/testing/rspec_helpers.rb
37
39
  - lib/code_teams/utils.rb
38
40
  homepage: https://github.com/rubyatscale/code_teams
39
41
  licenses:
@@ -57,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
59
  - !ruby/object:Gem::Version
58
60
  version: '0'
59
61
  requirements: []
60
- rubygems_version: 3.6.7
62
+ rubygems_version: 3.6.9
61
63
  specification_version: 4
62
64
  summary: A low-dependency gem for declaring and querying engineering teams
63
65
  test_files: []