gem_kit 0.1.0 → 0.2.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: 34f2ba71222409206c90f3399be50f301388c0c0afdfff07558337696d246d08
4
- data.tar.gz: bd16ae352402059e8852e1bcc5504e61b1961cbba172445837ba8e2a2c46bd31
3
+ metadata.gz: 4f7e45b6e07e8d99724e3bff85fdbc2d810a0fed2d4df6b31479f8df07741256
4
+ data.tar.gz: 96592d7cc445bc734f2c7645605360ee658e578916a5e3b957fb6984b528f5ab
5
5
  SHA512:
6
- metadata.gz: e6bc7f8f8824e3e51f3b0e56df2d0832f698712c5b841e61f68803df7b30d920e29163fea4a5fe1aa78432e1a01b7b53ee54d92d48c37193af6d09678356d211
7
- data.tar.gz: 2bc3b1f4b7f04f02d1fe6aa243ac18073b6d1db380d7e42a6999bf17c44e2f1638fb60fe3948dadd445e5163b3c47d7b24d472563d5ad7a0517886e911b9261c
6
+ metadata.gz: 15fea17b107de0fdb5c22e9c30c594689cb88b2fd91b0e47f1987694e82f5aa9725c168a113e0f020061c2cccddd81e83bc5d785d85fb90ea6e41e8df9525ce2
7
+ data.tar.gz: 491a1a5290db36b81ef45d51090ae0f8707c517283c72a268887e3b573344d5bce52ea3abc6b902d73341c0acca7237437586631b088cb84e10dd980d0d58f5a
data/README.md CHANGED
@@ -1,50 +1,191 @@
1
1
  # gem_kit
2
2
 
3
- TODO: Write a description.
3
+ Two gems, split along the line that matters: **what a library needs while it is
4
+ running**, and **what its maintainer needs while releasing it**.
4
5
 
5
- ## Install
6
+ | Gem | Half | Depends on |
7
+ | --- | --- | --- |
8
+ | [`gem_kit`](gem_kit.gemspec) | `GemKit::Deprecate` — declare a deprecation | nothing beyond RubyGems |
9
+ | [`gem_kit-release`](gem_kit-release.gemspec) | `gem kit …` — enforce it at release time | `gem_kit`, `thor` |
10
+
11
+ A deprecation is a **dated promise**: it names its replacement *and* the version
12
+ the old name stops existing in. The promise is declared at runtime and enforced
13
+ at release time — which is exactly why these are two gems. A library that
14
+ deprecates a name should not thereby acquire a release toolchain, and only the
15
+ enforcing end needs one installed.
6
16
 
7
17
  ```ruby
8
- gem install gem_kit
18
+ # Gemfile
19
+ gem "gem_kit" # runtime: declaring
20
+ gem "gem_kit-release", group: :development # release: enforcing
9
21
  ```
10
22
 
11
- ## Usage
23
+ ## Declaring — gem_kit
24
+
25
+ A method:
12
26
 
13
- ```bash
14
- TODO: Write usage instructions.
27
+ ```ruby
28
+ class Session
29
+ extend GemKit::Deprecate
30
+
31
+ def old_reset = new_reset
32
+ deprecate :old_reset, "Session#new_reset", "5.0"
33
+ end
15
34
  ```
16
35
 
17
- ## Development
36
+ A renamed or moved constant — leave the old name as a subclass of the new one:
18
37
 
19
- After checking out the repo:
38
+ ```ruby
39
+ class Completion < New::Completion
40
+ extend GemKit::Deprecate
41
+ superseded_by "New::Completion", "5.0"
42
+ end
43
+ ```
20
44
 
21
- ```bash
22
- bin/setup
23
- bin/test
45
+ Both keep working, warn on use naming the caller, and register the deadline:
46
+
47
+ ```
48
+ NOTE: Session#old_reset is deprecated; use Session#new_reset instead.
49
+ It will be removed in 5.0
50
+ Session#old_reset called from app.rb:12.
51
+ ```
52
+
53
+ Use `:none` when there genuinely is no replacement. `Gem::Deprecate.skip_during`
54
+ silences the warnings, so a suite can exercise the old path in quiet.
55
+
56
+ ## Enforcing — gem_kit-release
57
+
58
+ Installing it registers one `gem` command. There is nothing to wire up — no
59
+ Rakefile, no binstubs:
60
+
61
+ ```sh
62
+ gem kit setup # write DEPRECATIONS.md and RELEASE.md into your project
63
+ gem kit bump minor # move the version
64
+ gem kit changelog --write # have an AI CLI write the entry
65
+ gem kit changelog # lint it
66
+ gem kit deprecations # what is still outstanding
67
+ gem kit release --dry-run # run the gates
68
+ gem kit tag --push # tag it
69
+ ```
70
+
71
+ | Command | What it does |
72
+ | --- | --- |
73
+ | `gem kit setup` | Writes DEPRECATIONS.md and RELEASE.md, rendered for your gem's name and versions. A generator, so `--force`, `--skip` and `--pretend` all work. |
74
+ | `gem kit bump <major\|minor\|patch>` | Rewrites the version file. Refuses to bump onto a deprecation deadline; `--force` overrides. |
75
+ | `gem kit changelog [VERSION]` | Lints CHANGELOG.md. With a version, checks that version is ready to release. |
76
+ | `gem kit changelog --write` | Hands the entry to the configured AI CLI. |
77
+ | `gem kit deprecations [VERSION]` | Lists what is outstanding. With a version, fails if any come due — a CI gate. |
78
+ | `gem kit release [--dry-run]` | Gates, then builds and pushes. |
79
+ | `gem kit tag [--push]` | Tags `v<version>`, refusing if it exists. |
80
+
81
+ Everything sits behind one `gem` command rather than six, so nothing here can
82
+ collide with a command RubyGems ships now or adds later — `gem check`,
83
+ `gem build`, `gem push` and `gem setup` all already exist.
84
+
85
+ The command line is [Thor](https://github.com/rails/thor), so `gem kit` lists
86
+ the commands and `gem kit help <command>` prints one command's arguments and
87
+ options. `gem kit setup` is a `Thor::Group` generator — the same machinery
88
+ behind `rails generate` — which is where its `create` / `identical` /
89
+ `conflict` reporting and its `--force`, `--skip` and `--pretend` come from.
90
+
91
+ Everything is read out of your `.gemspec`. Override only what cannot be
92
+ inferred:
93
+
94
+ ```ruby
95
+ GemKit::Release.configure do |config|
96
+ config.changelog = "HISTORY.md" # default: CHANGELOG.md
97
+ config.version_file = "lib/x.rb" # default: lib/<name>/version.rb
98
+ config.require_path = "x" # default: <name>, hyphens as slashes
99
+ config.test_command = "bin/test"
100
+ config.changelog_writer = "claude" # the CLI that writes the entry
101
+ end
24
102
  ```
25
103
 
26
- ### Versioning
104
+ ### In a repository with more than one gemspec
27
105
 
28
- Increment the version:
106
+ Like this one. Every command takes `--gem`, and refuses to guess without it:
29
107
 
30
- ```bash
31
- bin/increment-version patch # 0.1.0 -> 0.1.1
32
- bin/increment-version minor # 0.1.0 -> 0.2.0
33
- bin/increment-version major # 0.1.0 -> 1.0.0
108
+ ```sh
109
+ $ gem kit changelog
110
+ 2 gemspecs in /home/you/src/gem_kit (gem_kit-release, gem_kit); name one with --gem
111
+
112
+ $ gem kit changelog --gem gem_kit
113
+ CHANGELOG-gem_kit.md is clean.
34
114
  ```
35
115
 
36
- ### Releasing
116
+ Three things follow from a repository holding several gems, and `gem kit`
117
+ arranges all three itself:
118
+
119
+ | | One gem | Several |
120
+ | --- | --- | --- |
121
+ | Changelog | `CHANGELOG.md` | `CHANGELOG-<gem>.md` |
122
+ | Release document | `RELEASE.md` | `RELEASE-<gem>.md` |
123
+ | Tag | `v1.2.3` | `<gem>-v1.2.3` |
124
+
125
+ The changelog has to be per-gem: the release gate asks whether the version
126
+ being cut is the *topmost* released section, and two gems interleaved in one
127
+ file means the older one never is — so the second gem could never be released.
128
+ The tag has to be per-gem because `v1.2.3` says which version but not which
129
+ gem, which is fine until two of them are at the same one. `DEPRECATIONS.md` is
130
+ written once for the repository, because one deprecation policy governs
131
+ everything in it.
132
+
133
+ ## Extending it
134
+
135
+ `gem kit` takes commands from other gems. `GemKit::Release.plugin` is the seam:
37
136
 
38
- Build and push to rubygems.org (checks the remote version first):
137
+ ```ruby
138
+ # lib/gem_kit/plugin.rb, in a gem of your own
139
+ require "gem_kit/release/cli"
39
140
 
40
- ```bash
41
- bin/release-gem
141
+ GemKit::Release.plugin do
142
+ desc "lint", "Check this gem for the things gems get wrong"
143
+ def lint = GemKit::Plugin::Lint.new(options).call
144
+ end
42
145
  ```
43
146
 
44
- ## Contributing
147
+ The block is evaluated on the Thor class, so the whole Thor DSL is in scope —
148
+ `desc`, `long_desc`, `method_option`, `map`, and `register` for a `Thor::Group`
149
+ generator. A command added this way is indistinguishable from a built-in one:
150
+ it appears in `gem kit`, takes `--gem`, and gets a help page.
151
+
152
+ Ship a `lib/rubygems_plugin.rb` that requires your file and RubyGems loads it on
153
+ every `gem` invocation, the same way this gem is loaded. See
154
+ [gem_kit-plugin](https://github.com/n-at-han-k/gem_kit-plugin) for a worked
155
+ example.
156
+
157
+ ## Layout
45
158
 
46
- Bug reports and pull requests are welcome on GitHub.
159
+ ```
160
+ lib/gem_kit.rb gem_kit
161
+ lib/gem_kit/deprecate.rb
162
+ lib/gem_kit/release.rb gem_kit-release
163
+ lib/gem_kit/release/
164
+ lib/rubygems_plugin.rb
165
+ template/ the gem template this repository used to be
166
+ ```
167
+
168
+ One `Gemfile`, one `Gemfile.lock`, one `gemset.nix`, one `flake.nix` and one
169
+ `lefthook.yml` cover both gems. The dependencies are listed in the Gemfile
170
+ outright rather than through `gemspec`, which is the only sane answer when two
171
+ gemspecs share one bundle — and, separately, what `bundlerEnv` needs, since it
172
+ resolves against a store directory holding only a Gemfile and a lockfile.
173
+
174
+ Each gemspec names its own files rather than globbing `lib/**/*.rb`, because a
175
+ glob would put the whole release toolchain inside the runtime gem.
176
+
177
+ ## Template
178
+
179
+ `template/` holds the gem template this repository started as: clone it, run
180
+ `bin/01-rename-gem`, and you have a gem.
181
+
182
+ ## Development
183
+
184
+ ```sh
185
+ direnv allow # or: nix develop
186
+ bin/test
187
+ ```
47
188
 
48
189
  ## License
49
190
 
50
- The gem is available as open source under the terms of the [MIT License](LICENSE).
191
+ MIT
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/deprecate"
4
+
5
+ module GemKit
6
+ # A deprecation is a dated promise: it names the replacement *and* the
7
+ # version the old name stops existing in. Built on Gem::Deprecate, which
8
+ # gets the message format and the skip_during escape hatch right, plus one
9
+ # addition — a registry, so the set of outstanding promises is data the
10
+ # release tooling can enforce rather than prose someone has to remember.
11
+ #
12
+ # Deprecate a method:
13
+ #
14
+ # class Session
15
+ # extend GemKit::Deprecate
16
+ #
17
+ # def old_reset = new_reset
18
+ # deprecate :old_reset, "Session#new_reset", "5.0"
19
+ # end
20
+ #
21
+ # Deprecate a whole constant that has moved or been renamed — leave the old
22
+ # name in place as a subclass of the new one, then declare it:
23
+ #
24
+ # class Completion < Brute::Completion::OpenRouter
25
+ # extend GemKit::Deprecate
26
+ # superseded_by "Brute::Completion::OpenRouter", "5.0"
27
+ # end
28
+ #
29
+ # Both warn on use, naming the caller. Gem::Deprecate.skip_during silences
30
+ # them, so a test suite can exercise the old path in quiet.
31
+ module Deprecate
32
+ extend Gem::Deprecate
33
+
34
+ # One outstanding deprecation. `removed_in` is the deadline the release
35
+ # gate reads.
36
+ Entry = Struct.new(:name, :replacement, :removed_in, :declared_at, keyword_init: true) do
37
+ def to_s
38
+ "#{name} -> #{replacement == :none ? "(no replacement)" : replacement}"
39
+ end
40
+ end
41
+
42
+ class << self
43
+ # Every deprecation declared in the loaded library, in declaration order.
44
+ def registry
45
+ @registry ||= []
46
+ end
47
+
48
+ def register(name:, replacement:, removed_in:, declared_at: nil)
49
+ entry = Entry.new(
50
+ name: name.to_s,
51
+ replacement: replacement,
52
+ removed_in: Gem::Version.new(removed_in.to_s),
53
+ declared_at: declared_at || location(1),
54
+ )
55
+ registry << entry
56
+ entry
57
+ end
58
+
59
+ # The deprecations that come due at `version` — every deadline that has
60
+ # arrived or passed. Releasing `version` with any of these still in the
61
+ # tree breaks the promise the warning made.
62
+ def pending(version)
63
+ target = Gem::Version.new(version.to_s)
64
+ registry.select { |entry| entry.removed_in <= target }
65
+ end
66
+
67
+ # Deprecations still inside their grace period at `version`.
68
+ def upcoming(version)
69
+ target = Gem::Version.new(version.to_s)
70
+ registry.reject { |entry| entry.removed_in <= target }
71
+ end
72
+
73
+ # Single funnel for every warning: Gem::Deprecate.skip_during works
74
+ # across all of them, and specs have one place to listen.
75
+ def warn(message)
76
+ Kernel.warn(message) unless Gem::Deprecate.skip
77
+ end
78
+
79
+ # The Gem::Deprecate-shaped message. `origin` must be computed at the
80
+ # call site — one frame deeper and it names this file rather than the
81
+ # code that needs changing.
82
+ def message(target, replacement, removed_in, origin)
83
+ [
84
+ "NOTE: #{target} is deprecated",
85
+ replacement == :none ? " with no replacement" : "; use #{replacement} instead",
86
+ ". It will be removed in #{removed_in}",
87
+ "\n#{target} called from #{origin}.",
88
+ ].join
89
+ end
90
+
91
+ def location(depth)
92
+ caller_locations(depth + 1, 1)&.first&.then { |l| "#{l.path}:#{l.lineno}" }
93
+ end
94
+ end
95
+
96
+ # Deprecate one method. Mirrors Gem::Deprecate#rubygems_deprecate, but the
97
+ # deadline is explicit — a deprecation added late in a cycle usually wants
98
+ # the major after next, and guessing that is not the tool's business.
99
+ def deprecate(name, replacement, removed_in)
100
+ label = singleton_class? ? "#{attached_object}.#{name}" : "#{self}##{name}"
101
+ Deprecate.register(name: label, replacement: replacement, removed_in: removed_in,
102
+ declared_at: Deprecate.location(1))
103
+
104
+ class_eval do
105
+ old = "_deprecated_#{name}"
106
+ alias_method old, name
107
+ define_method name do |*args, &block|
108
+ target = is_a?(Module) ? "#{self}.#{name}" : "#{self.class}##{name}"
109
+ origin = Gem.location_of_caller.join(":")
110
+ Deprecate.warn(Deprecate.message(target, replacement, removed_in, origin))
111
+ send(old, *args, &block)
112
+ end
113
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
114
+ end
115
+ end
116
+
117
+ # Deprecate the constant this is called in — the renamed-or-moved case.
118
+ # Named `superseded_by` rather than `deprecate_constant` because Module
119
+ # already has a method by that name and shadowing it would be rude.
120
+ def superseded_by(replacement, removed_in)
121
+ Deprecate.register(name: name || to_s, replacement: replacement, removed_in: removed_in,
122
+ declared_at: Deprecate.location(1))
123
+
124
+ return unless respond_to?(:new)
125
+
126
+ define_singleton_method(:new) do |*args, **options, &block|
127
+ origin = Gem.location_of_caller.join(":")
128
+ Deprecate.warn(Deprecate.message(name || to_s, replacement, removed_in, origin))
129
+ super(*args, **options, &block)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ __END__
136
+
137
+ describe "gem_kit/deprecate" do
138
+ Deprecate = GemKit::Deprecate unless defined?(Deprecate)
139
+
140
+ captured = []
141
+ # Capture what Deprecate.warn emits and keep the shared registry clean —
142
+ # these specs declare throwaway deprecations.
143
+ isolated = lambda do |&block|
144
+ saved = Deprecate.registry.dup
145
+ original = Deprecate.method(:warn)
146
+ captured.clear
147
+ Deprecate.define_singleton_method(:warn) { |message| captured << message }
148
+ begin
149
+ block.call
150
+ ensure
151
+ Deprecate.define_singleton_method(:warn, original)
152
+ Deprecate.registry.replace(saved)
153
+ end
154
+ end
155
+
156
+ it "warns on a deprecated method, naming replacement, version and caller" do
157
+ isolated.call do
158
+ klass = Class.new do
159
+ extend Deprecate
160
+ def new_name = :result
161
+ def old_name = new_name
162
+ deprecate :old_name, "Thing#new_name", "9.0"
163
+ end
164
+
165
+ klass.new.old_name.should == :result # still works
166
+ captured.size.should == 1
167
+ captured.first.should.match(/is deprecated/)
168
+ captured.first.should.match(/use Thing#new_name instead/)
169
+ captured.first.should.match(/removed in 9\.0/)
170
+ captured.first.should.match(/called from /)
171
+ end
172
+ end
173
+
174
+ it "names the caller, not the deprecation machinery" do
175
+ isolated.call do
176
+ klass = Class.new do
177
+ extend Deprecate
178
+ def old_name = :result
179
+ deprecate :old_name, "Thing#new_name", "9.0"
180
+ end
181
+
182
+ # These specs live in this file's __END__, so "the caller" is a line in
183
+ # deprecate.rb either way — pin the exact line to tell them apart.
184
+ klass.new.old_name; call_line = __LINE__
185
+ captured.first.should.match(/called from .*deprecate\.rb:#{call_line}\./)
186
+ end
187
+ end
188
+
189
+ it "labels a class-method deprecation by the class, not its singleton" do
190
+ isolated.call do
191
+ Class.new do
192
+ def self.to_s = "Demo"
193
+ def self.old_thing = :ok
194
+ class << self
195
+ extend Deprecate
196
+ deprecate :old_thing, "Other.new_thing", "9.0"
197
+ end
198
+ end
199
+
200
+ Deprecate.registry.last.name.should == "Demo.old_thing"
201
+ end
202
+ end
203
+
204
+ it "warns on a superseded constant but keeps it working" do
205
+ isolated.call do
206
+ modern = Class.new { def initialize(x); @x = x; end; attr_reader :x }
207
+ legacy = Class.new(modern) do
208
+ extend Deprecate
209
+ def self.name = "Old::Name"
210
+ superseded_by "New::Name", "9.0"
211
+ end
212
+
213
+ legacy.new(42).x.should == 42 # still works
214
+ captured.size.should == 1
215
+ captured.first.should.match(/Old::Name is deprecated; use New::Name instead/)
216
+ end
217
+ end
218
+
219
+ it "supports :none for a deprecation with no replacement" do
220
+ isolated.call do
221
+ klass = Class.new do
222
+ extend Deprecate
223
+ def gone = :ok
224
+ deprecate :gone, :none, "9.0"
225
+ end
226
+
227
+ klass.new.gone
228
+ captured.first.should.match(/with no replacement/)
229
+ end
230
+ end
231
+
232
+ it "registers each declaration with its deadline and source" do
233
+ isolated.call do
234
+ Class.new do
235
+ extend Deprecate
236
+ def gone = nil
237
+ deprecate :gone, "Other#kept", "9.0"
238
+ end
239
+
240
+ entry = Deprecate.registry.last
241
+ entry.replacement.should == "Other#kept"
242
+ entry.removed_in.should == Gem::Version.new("9.0")
243
+ entry.declared_at.should.match(/deprecate\.rb:\d+/)
244
+ end
245
+ end
246
+
247
+ it "splits the registry into pending and upcoming at a version" do
248
+ isolated.call do
249
+ Deprecate.registry.clear
250
+ Deprecate.register(name: "A", replacement: "A2", removed_in: "5.0")
251
+ Deprecate.register(name: "B", replacement: "B2", removed_in: "6.0")
252
+
253
+ Deprecate.pending("5.0.0").map(&:name).should == ["A"]
254
+ Deprecate.upcoming("5.0.0").map(&:name).should == ["B"]
255
+ Deprecate.pending("4.9.0").should.be.empty
256
+ Deprecate.pending("6.1.0").map(&:name).should == ["A", "B"]
257
+ end
258
+ end
259
+
260
+ it "stays quiet inside Gem::Deprecate.skip_during" do
261
+ saved = Deprecate.registry.dup
262
+ begin
263
+ klass = Class.new do
264
+ extend Deprecate
265
+ def quiet = :ok
266
+ deprecate :quiet, "Other#loud", "9.0"
267
+ end
268
+
269
+ warned = []
270
+ original = Kernel.method(:warn)
271
+ Kernel.define_singleton_method(:warn) { |*args| warned << args.join }
272
+ begin
273
+ Gem::Deprecate.skip_during { klass.new.quiet.should == :ok }
274
+ ensure
275
+ Kernel.define_singleton_method(:warn, original)
276
+ end
277
+
278
+ warned.should.be.empty
279
+ ensure
280
+ Deprecate.registry.replace(saved)
281
+ end
282
+ end
283
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemKit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/gem_kit.rb CHANGED
@@ -1,6 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "gem_kit/version"
4
+ require_relative "gem_kit/deprecate"
4
5
 
6
+ # The runtime half of the kit: what a library needs while it is *running*, as
7
+ # opposed to what its maintainer needs while releasing it.
8
+ #
9
+ # Right now that is one thing — GemKit::Deprecate, the deprecation DSL. It has
10
+ # no dependencies beyond RubyGems' own, deliberately: a library that deprecates
11
+ # a name should not thereby acquire a release toolchain.
12
+ #
13
+ # class Session
14
+ # extend GemKit::Deprecate
15
+ #
16
+ # def old_reset = new_reset
17
+ # deprecate :old_reset, "Session#new_reset", "5.0"
18
+ # end
19
+ #
20
+ # The release half lives in the gem_kit-release gem, in this same repository:
21
+ # `gem kit bump|changelog|deprecations|release|tag`. It reads the registry this
22
+ # one populates, which is the whole point of the split — the promises are
23
+ # declared at runtime and enforced at release time, and only the enforcing end
24
+ # needs the tooling installed.
5
25
  module GemKit
6
26
  end
metadata CHANGED
@@ -1,95 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Nathan K
8
- bindir: exe
7
+ - Nathan Kidd
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-01 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: minitest
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '5.0'
19
- type: :development
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '5.0'
26
- - !ruby/object:Gem::Dependency
27
- name: rake
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '13.0'
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '13.0'
40
- - !ruby/object:Gem::Dependency
41
- name: rubocop
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '1.21'
47
- type: :development
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.21'
54
- description: 'Clone the repo and run bin/rename-gem and you have a gem.
11
+ dependencies: []
12
+ description: |
13
+ A deprecation is a dated promise: it names its replacement and the version
14
+ the old name stops existing in. GemKit::Deprecate declares that promise --
15
+ `deprecate` for a method, `superseded_by` for a renamed constant -- warns
16
+ on use naming the caller, and registers the deadline so it can be checked.
55
17
 
56
- '
57
- email:
58
- - nathankidd@hey.com
59
- executables:
60
- - gem_kit
18
+ Built on Gem::Deprecate, so the message format and Gem::Deprecate.skip_during
19
+ work as they already do. No dependencies beyond RubyGems' own: a library
20
+ that deprecates a name should not thereby acquire a release toolchain.
21
+
22
+ The checking lives in the gem_kit-release gem, which reads this registry.
23
+ email: nathanblenheimkidd@gmail.com
24
+ executables: []
61
25
  extensions: []
62
26
  extra_rdoc_files: []
63
27
  files:
64
- - ".envrc"
65
- - ".gitignore"
66
- - Gemfile
67
28
  - LICENSE
68
29
  - README.md
69
- - Rakefile
70
- - bin/choose-license
71
- - bin/console
72
- - bin/increment-version
73
- - bin/release-gem
74
- - bin/rename-gem
75
- - bin/setup
76
- - bin/test
77
- - bin/update-spec
78
- - exe/gem_kit
79
- - flake.lock
80
- - flake.nix
81
- - gem_kit.gemspec
82
- - gem_kit.gemspec.erb
83
30
  - lib/gem_kit.rb
31
+ - lib/gem_kit/deprecate.rb
84
32
  - lib/gem_kit/version.rb
85
- - lib/gem_kit/version.rb.erb
86
- homepage: https://github.com/n-at-han-k/gem-kit
33
+ homepage: https://github.com/n-at-han-k/gem_kit
87
34
  licenses:
88
35
  - MIT
89
36
  metadata:
90
- homepage_uri: https://github.com/n-at-han-k/gem-kit
91
- source_code_uri: https://github.com/n-at-han-k/gem-kit
92
- documentation_uri: https://github.com/n-at-han-k/gem-kit
37
+ source_code_uri: https://github.com/n-at-han-k/gem_kit
38
+ changelog_uri: https://github.com/n-at-han-k/gem_kit/blob/main/CHANGELOG.md
39
+ bug_tracker_uri: https://github.com/n-at-han-k/gem_kit/issues
93
40
  rubygems_mfa_required: 'true'
94
41
  rdoc_options: []
95
42
  require_paths:
@@ -98,7 +45,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
98
45
  requirements:
99
46
  - - ">="
100
47
  - !ruby/object:Gem::Version
101
- version: 3.2.0
48
+ version: '3.2'
102
49
  required_rubygems_version: !ruby/object:Gem::Requirement
103
50
  requirements:
104
51
  - - ">="
@@ -107,5 +54,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
54
  requirements: []
108
55
  rubygems_version: 3.7.2
109
56
  specification_version: 4
110
- summary: Dynamic gem template
57
+ summary: A deprecation DSL that makes a removal deadline enforceable
111
58
  test_files: []
data/.envrc DELETED
@@ -1 +0,0 @@
1
- use flake
data/.gitignore DELETED
@@ -1,7 +0,0 @@
1
- *.gem
2
- /.bundle/
3
- /coverage/
4
- /pkg/
5
- /tmp/
6
- /vendor/bundle
7
- Gemfile.lock
data/Gemfile DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
data/Rakefile DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/testtask"
4
-
5
- Rake::TestTask.new(:test) do |t|
6
- t.libs << "test"
7
- t.libs << "lib"
8
- t.test_files = FileList["test/**/*_test.rb"]
9
- end
10
-
11
- task default: :test
data/bin/choose-license DELETED
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "json"
5
- require "net/http"
6
- require "uri"
7
-
8
- def fetch_json(url)
9
- JSON.parse(Net::HTTP.get(URI(url)))
10
- end
11
-
12
- licenses = fetch_json("https://api.github.com/licenses")
13
-
14
- licenses.each_with_index { |l, i| puts "#{i + 1}. #{l['name']}" }
15
-
16
- print "\nPick a license: "
17
- choice = licenses[gets.to_i - 1]
18
-
19
- abort("Invalid choice") unless choice
20
-
21
- detail = fetch_json(choice["url"])
22
-
23
- license_path = File.expand_path("../LICENSE", __dir__)
24
- File.write(license_path, detail["body"])
25
-
26
- # Update the SPDX ID in the gemspec
27
- spdx_id = choice["spdx_id"]
28
- gemspec_path = Dir.glob(File.expand_path("../*.gemspec", __dir__)).first
29
- if gemspec_path
30
- content = File.read(gemspec_path)
31
- content.sub!(/spec\.license\s*=\s*"[^"]*"/, "spec.license = \"#{spdx_id}\"")
32
- File.write(gemspec_path, content)
33
- end
34
-
35
- puts "Wrote #{spdx_id} license to LICENSE"
data/bin/console DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "bundler/setup"
5
- require "gem_kit"
6
- require "irb"
7
-
8
- IRB.start(__FILE__)
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "erb"
5
- require_relative "../lib/gem_kit/version"
6
-
7
- USAGE = <<~TEXT
8
- Usage: bin/increment-version <major|minor|patch>
9
- TEXT
10
-
11
- segment = ARGV[0]
12
-
13
- unless %w[major minor patch].include?(segment)
14
- warn USAGE
15
- exit 1
16
- end
17
-
18
- current = GemKit::VERSION
19
- major, minor, patch = current.split(".").map(&:to_i)
20
-
21
- case segment
22
- when "major"
23
- major += 1
24
- minor = 0
25
- patch = 0
26
- when "minor"
27
- minor += 1
28
- patch = 0
29
- when "patch"
30
- patch += 1
31
- end
32
-
33
- version = "#{major}.#{minor}.#{patch}"
34
-
35
- template_path = File.expand_path("../lib/gem_kit/version.rb.erb", __dir__)
36
- output_path = File.expand_path("../lib/gem_kit/version.rb", __dir__)
37
-
38
- template = ERB.new(File.read(template_path))
39
- result = template.result(binding)
40
-
41
- File.write(output_path, result)
42
-
43
- puts "#{current} -> #{version}"
data/bin/release-gem DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative "../lib/gem_kit/version"
5
-
6
- local_version = GemKit::VERSION
7
- gem_name = "gem_kit"
8
- gemspec = "gem_kit.gemspec"
9
-
10
- puts "Local version: #{local_version}"
11
-
12
- remote_output = `gem specification #{gem_name} version --remote 2>&1`
13
-
14
- if $?.success?
15
- remote_version = remote_output[/version: (.+)/, 1]&.strip
16
- puts "Remote version: #{remote_version}"
17
-
18
- if Gem::Version.new(local_version) <= Gem::Version.new(remote_version)
19
- abort "ERROR: Local version (#{local_version}) has not been incremented past remote (#{remote_version})"
20
- end
21
- else
22
- puts "Gem not yet published remotely, proceeding with first release"
23
- end
24
-
25
- puts "Building #{gemspec}..."
26
- system("gem build #{gemspec}") || abort("ERROR: gem build failed")
27
-
28
- gem_file = "#{gem_name}-#{local_version}.gem"
29
- puts "Pushing #{gem_file}..."
30
- system("gem push #{gem_file}") || abort("ERROR: gem push failed")
31
-
32
- puts "Released #{gem_name} #{local_version}"
data/bin/rename-gem DELETED
@@ -1,104 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "fileutils"
5
-
6
- current_name = "gem_kit"
7
-
8
- def to_camel(snake)
9
- snake.split("_").map(&:capitalize).join
10
- end
11
-
12
- # Parse --from flag
13
- args = ARGV.dup
14
- if (idx = args.index("--from"))
15
- current_name = args.delete_at(idx + 1)&.tr("-", "_")
16
- args.delete_at(idx)
17
- abort "ERROR: --from requires a gem name" unless current_name && !current_name.empty?
18
- end
19
-
20
- name = args[0]
21
-
22
- if name.nil? || name.empty?
23
- print "Gem name (snake_case): "
24
- name = $stdin.gets.chomp
25
- end
26
-
27
- abort "Usage: bin/rename-gem <gem_name> [--from <current_name>]" if name.empty?
28
-
29
- old_snake = current_name
30
- old_camel = to_camel(old_snake)
31
-
32
- snake = name.tr("-", "_")
33
- camel = to_camel(snake)
34
-
35
- if snake == old_snake
36
- abort "Already named #{snake}, nothing to do"
37
- end
38
-
39
- root = File.expand_path("..", __dir__)
40
-
41
- # --- Rename files and directories ---
42
-
43
- renames = {
44
- "lib/#{old_snake}" => "lib/#{snake}",
45
- "lib/#{old_snake}.rb" => "lib/#{snake}.rb",
46
- "lib/#{snake}/version.rb" => "lib/#{snake}/version.rb",
47
- "lib/#{snake}/version.rb.erb" => "lib/#{snake}/version.rb.erb",
48
- "#{old_snake}.gemspec" => "#{snake}.gemspec",
49
- "#{old_snake}.gemspec.erb" => "#{snake}.gemspec.erb",
50
- "exe/#{old_snake}" => "exe/#{snake}",
51
- "test/#{old_snake}_test.rb" => "test/#{snake}_test.rb",
52
- }
53
-
54
- # Rename directory first (lib/old_name -> lib/new_name)
55
- old_dir = File.join(root, "lib", old_snake)
56
- new_dir = File.join(root, "lib", snake)
57
- if File.directory?(old_dir)
58
- FileUtils.mv(old_dir, new_dir)
59
- puts " mv lib/#{old_snake}/ -> lib/#{snake}/"
60
- end
61
-
62
- # Rename files
63
- renames.each do |old_rel, new_rel|
64
- next if old_rel.start_with?("lib/#{old_snake}/") # directory already moved
65
- next if old_rel == "lib/#{snake}/version.rb" # already inside renamed dir
66
-
67
- old_path = File.join(root, old_rel)
68
- new_path = File.join(root, new_rel)
69
- next unless File.exist?(old_path)
70
- next if old_path == new_path
71
-
72
- FileUtils.mv(old_path, new_path)
73
- puts " mv #{old_rel} -> #{new_rel}"
74
- end
75
-
76
- # --- Replace content in all files ---
77
-
78
- files = Dir.glob(File.join(root, "**/*"), File::FNM_DOTMATCH)
79
- .reject { |f| File.directory?(f) }
80
- .reject { |f| f.include?("/.git/") }
81
-
82
- files.each do |path|
83
- content = File.read(path)
84
- original = content.dup
85
-
86
- content.gsub!(old_camel, camel)
87
- content.gsub!(old_snake, snake)
88
-
89
- if content != original
90
- File.write(path, content)
91
- puts " updated #{path.sub("#{root}/", "")}"
92
- end
93
- end
94
-
95
- # Update current_name in this script to reflect the new name
96
- script_path = File.join(root, "bin", "rename-gem")
97
- if File.exist?(script_path)
98
- script = File.read(script_path)
99
- script.sub!(/^current_name = ".*"$/, "current_name = \"#{snake}\"")
100
- File.write(script_path, script)
101
- puts " updated bin/rename-gem (current_name = \"#{snake}\")"
102
- end
103
-
104
- puts "\nRenamed #{old_snake} -> #{snake} (#{old_camel} -> #{camel})"
data/bin/setup DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
-
5
- bundle install
data/bin/test DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- $LOAD_PATH.unshift File.expand_path("../test", __dir__)
5
-
6
- require "bundler/setup"
7
-
8
- Dir.chdir(File.expand_path("..", __dir__))
9
-
10
- if ARGV.empty?
11
- Dir.glob("test/**/*_test.rb").sort.each { |f| require_relative "../#{f}" }
12
- else
13
- ARGV.each { |f| require_relative "../#{f}" }
14
- end
data/bin/update-spec DELETED
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "erb"
5
- require_relative "../lib/gem_kit/version"
6
-
7
- gemspec_path = File.expand_path("../gem_kit.gemspec", __dir__)
8
- spec = Gem::Specification.load(gemspec_path)
9
-
10
- def prompt(label, default)
11
- print "#{label} [#{default}] (y/N): "
12
- return default unless $stdin.gets.chomp.downcase == "y"
13
-
14
- print "#{label}: "
15
- input = $stdin.gets.chomp
16
- input.empty? ? default : input
17
- end
18
-
19
- def prompt_script(label, current, script)
20
- print "#{label} [#{current}] (y/N): "
21
- return unless $stdin.gets.chomp.downcase == "y"
22
-
23
- system(File.expand_path(script, __dir__))
24
- end
25
-
26
- prompt_script("Gem name", spec.name, "rename-gem")
27
- authors = prompt("Authors (comma-separated)", spec.authors.join(", ")).split(",").map(&:strip)
28
- emails = prompt("Emails (comma-separated)", spec.email.join(", ")).split(",").map(&:strip)
29
- summary = prompt("Summary", spec.summary)
30
- description = prompt("Description", spec.description.strip)
31
- homepage = prompt("Homepage", spec.homepage)
32
- prompt_script("License", spec.license, "choose-license")
33
- ruby_version = prompt("Minimum Ruby version", spec.required_ruby_version.to_s.delete(">= "))
34
-
35
- # Reload spec in case rename-gem or choose-license changed things
36
- gemspec_path = Dir.glob(File.expand_path("../*.gemspec", __dir__)).first
37
- spec = Gem::Specification.load(gemspec_path)
38
- gem_name = spec.name
39
- license = spec.license
40
-
41
- template_path = gemspec_path.sub(/\.gemspec$/, ".gemspec.erb")
42
- output_path = gemspec_path
43
-
44
- template = ERB.new(File.read(template_path))
45
- result = template.result(binding)
46
-
47
- File.write(output_path, result)
48
-
49
- puts "Updated #{File.basename(gemspec_path)}"
data/exe/gem_kit DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative "../lib/gem_kit"
5
-
6
- puts "hello from gem_kit"
data/flake.lock DELETED
@@ -1,61 +0,0 @@
1
- {
2
- "nodes": {
3
- "nixpkgs": {
4
- "locked": {
5
- "lastModified": 1773821835,
6
- "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
7
- "owner": "NixOS",
8
- "repo": "nixpkgs",
9
- "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
10
- "type": "github"
11
- },
12
- "original": {
13
- "owner": "NixOS",
14
- "ref": "nixos-unstable",
15
- "repo": "nixpkgs",
16
- "type": "github"
17
- }
18
- },
19
- "root": {
20
- "inputs": {
21
- "nixpkgs": "nixpkgs",
22
- "utils": "utils"
23
- }
24
- },
25
- "systems": {
26
- "locked": {
27
- "lastModified": 1681028828,
28
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
29
- "owner": "nix-systems",
30
- "repo": "default",
31
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
32
- "type": "github"
33
- },
34
- "original": {
35
- "owner": "nix-systems",
36
- "repo": "default",
37
- "type": "github"
38
- }
39
- },
40
- "utils": {
41
- "inputs": {
42
- "systems": "systems"
43
- },
44
- "locked": {
45
- "lastModified": 1731533236,
46
- "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
47
- "owner": "numtide",
48
- "repo": "flake-utils",
49
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
50
- "type": "github"
51
- },
52
- "original": {
53
- "owner": "numtide",
54
- "repo": "flake-utils",
55
- "type": "github"
56
- }
57
- }
58
- },
59
- "root": "root",
60
- "version": 7
61
- }
data/flake.nix DELETED
@@ -1,37 +0,0 @@
1
- {
2
- description = "Ruby gem flake";
3
-
4
- inputs = {
5
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
- utils.url = "github:numtide/flake-utils";
7
- };
8
- outputs = { self, nixpkgs, utils }:
9
- utils.lib.eachDefaultSystem (system:
10
- let
11
- pkgs = nixpkgs.legacyPackages.${system};
12
- ruby = pkgs.ruby_3_4; # Specify version
13
- in
14
- {
15
- devShells.default = pkgs.mkShell {
16
- nativeBuildInputs = [
17
- pkgs.pkg-config # native extension discovery
18
- ];
19
-
20
- buildInputs = [
21
- ruby
22
- pkgs.libyaml # psych gem
23
- pkgs.openssl # openssl gem
24
- ];
25
-
26
- shellHook = ''
27
- export GEM_HOME="$PWD/.gem"
28
- export GEM_PATH="$GEM_HOME"
29
- export PATH="$GEM_HOME/bin:$PATH"
30
- export BUNDLE_PATH="$GEM_HOME"
31
- export BUNDLE_BIN="$GEM_HOME/bin"
32
- '';
33
- };
34
- }
35
- );
36
- }
37
-
data/gem_kit.gemspec DELETED
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/gem_kit/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "gem_kit"
7
- spec.version = GemKit::VERSION
8
- spec.authors = ["Nathan K"]
9
- spec.email = ["nathankidd@hey.com"]
10
-
11
- spec.summary = "Dynamic gem template"
12
-
13
- spec.description = <<~DESC
14
- Clone the repo and run bin/rename-gem and you have a gem.
15
- DESC
16
-
17
- spec.homepage = "https://github.com/n-at-han-k/gem-kit"
18
- spec.license = "MIT"
19
- spec.required_ruby_version = ">= 3.2.0"
20
-
21
- spec.metadata["homepage_uri"] = spec.homepage
22
- spec.metadata["source_code_uri"] = spec.homepage
23
- spec.metadata["documentation_uri"] = spec.homepage
24
- spec.metadata["rubygems_mfa_required"] = "true"
25
-
26
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|data)/}) }
27
- spec.bindir = "exe"
28
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
- spec.require_paths = ["lib"]
30
-
31
- spec.add_development_dependency "minitest", "~> 5.0"
32
- spec.add_development_dependency "rake", "~> 13.0"
33
- spec.add_development_dependency "rubocop", "~> 1.21"
34
- end
data/gem_kit.gemspec.erb DELETED
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/gem_kit/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "<%= gem_name %>"
7
- spec.version = GemKit::VERSION
8
- spec.authors = [<%= authors.map { |a| %("#{a}") }.join(", ") %>]
9
- spec.email = [<%= emails.map { |e| %("#{e}") }.join(", ") %>]
10
-
11
- spec.summary = "<%= summary %>"
12
-
13
- spec.description = <<~DESC
14
- <%= description %>
15
- DESC
16
-
17
- spec.homepage = "<%= homepage %>"
18
- spec.license = "<%= license %>"
19
- spec.required_ruby_version = ">= <%= ruby_version %>"
20
-
21
- spec.metadata["homepage_uri"] = spec.homepage
22
- spec.metadata["source_code_uri"] = spec.homepage
23
- spec.metadata["documentation_uri"] = spec.homepage
24
- spec.metadata["rubygems_mfa_required"] = "true"
25
-
26
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|data)/}) }
27
- spec.bindir = "exe"
28
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
- spec.require_paths = ["lib"]
30
-
31
- spec.add_development_dependency "minitest", "~> 5.0"
32
- spec.add_development_dependency "rake", "~> 13.0"
33
- spec.add_development_dependency "rubocop", "~> 1.21"
34
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemKit
4
- VERSION = "<%= version %>"
5
- end