diff --git a/Gemfile.lock b/Gemfile.lock index f434b50..8e034be 100755 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,24 @@ GIT remote: https://github.com/pboling/appraisal - revision: f5b830cb783ecb83e271c18c29c827af4986578d + revision: a3a3e4b7db67d9b085f96b2ffddd2b51bd8a1196 branch: galtzo specs: appraisal (3.0.0.rc1) bundler (>= 1.17.3) rake (>= 10) - thor (>= 0.14.0) + thor (>= 0.14) PATH remote: . specs: - version_gem (1.1.6) + version_gem (1.1.7) GEM remote: https://rubygems.org/ specs: ansi (1.5.0) - ast (2.4.2) - backports (3.25.0) + ast (2.4.3) + backports (3.25.1) benchmark (0.4.0) bigdecimal (3.1.9) bundler-audit (0.9.2) @@ -30,7 +30,7 @@ GEM debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) - diff-lcs (1.6.0) + diff-lcs (1.6.1) diffy (3.4.3) docile (1.4.1) dry-configurable (1.3.0) @@ -47,13 +47,13 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-schema (1.13.4) + dry-schema (1.14.1) concurrent-ruby (~> 1.0) dry-configurable (~> 1.0, >= 1.0.1) - dry-core (~> 1.0, < 2) - dry-initializer (~> 3.0) - dry-logic (>= 1.4, < 2) - dry-types (>= 1.7, < 2) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) zeitwerk (~> 2.6) dry-types (1.8.2) bigdecimal (~> 3.0) @@ -64,12 +64,12 @@ GEM zeitwerk (~> 2.6) github-markup (5.0.1) io-console (0.8.0) - irb (1.15.1) + irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.10.1) - kettle-soup-cover (1.0.4) + json (2.10.2) + kettle-soup-cover (1.0.5) simplecov (~> 0.22) simplecov-cobertura (~> 2.1) simplecov-console (~> 0.9, >= 0.9.1) @@ -80,16 +80,17 @@ GEM version_gem (~> 1.1, >= 1.1.4) language_server-protocol (3.17.0.4) lint_roller (1.1.0) - logger (1.6.6) + logger (1.7.0) method_source (1.1.0) ostruct (0.6.1) parallel (1.26.3) - parser (3.3.7.1) + parser (3.3.8.0) ast (~> 2.4.1) racc pp (0.6.2) prettyprint prettyprint (0.2.0) + prism (1.4.0) pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) @@ -99,17 +100,17 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.2.1) - rdoc (6.12.0) + rdoc (6.13.1) psych (>= 4.0.0) - redcarpet (3.6.0) - reek (6.4.0) - dry-schema (~> 1.13.0) + redcarpet (3.6.1) + reek (6.5.0) + dry-schema (~> 1.13) logger (~> 1.6) parser (~> 3.3.0) rainbow (>= 2.0, < 4.0) rexml (~> 3.1) regexp_parser (2.10.0) - reline (0.6.0) + reline (0.6.1) io-console (~> 0.5) rexml (3.4.1) rspec (3.13.0) @@ -126,18 +127,20 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.2) - rubocop (1.71.2) + rubocop (1.75.2) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.0) - parser (>= 3.3.1.0) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) rubocop-gradual (0.3.6) diff-lcs (>= 1.2.0, < 2.0) diffy (~> 3.0) @@ -150,15 +153,19 @@ GEM version_gem (>= 1.1.2, < 3) rubocop-md (1.2.4) rubocop (>= 1.45) - rubocop-packaging (0.5.2) - rubocop (>= 1.33, < 2.0) - rubocop-performance (1.23.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rake (0.6.0) - rubocop (~> 1.0) - rubocop-rspec (3.4.0) - rubocop (~> 1.61) + rubocop-packaging (0.6.0) + lint_roller (~> 1.1.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) rubocop-ruby2_2 (2.0.5) rubocop-gradual (~> 0.3, >= 0.3.1) rubocop-md (~> 1.2) @@ -167,10 +174,11 @@ GEM rubocop-thread_safety (~> 0.5, >= 0.5.1) standard-rubocop-lts (~> 1.0, >= 1.0.7) version_gem (>= 1.1.3, < 3) - rubocop-shopify (2.15.1) - rubocop (~> 1.51) - rubocop-thread_safety (0.6.0) - rubocop (>= 1.48.1) + rubocop-shopify (2.16.0) + rubocop (~> 1.62) + rubocop-thread_safety (0.7.2) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) ruby-progressbar (1.13.0) simplecov (0.22.0) docile (~> 1.1) @@ -188,18 +196,18 @@ GEM simplecov-rcov (0.3.7) simplecov (>= 0.4.1) simplecov_json_formatter (0.1.4) - standard (1.45.0) + standard (1.49.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.71.0) + rubocop (~> 1.75.2) standard-custom (~> 1.0.0) - standard-performance (~> 1.6) + standard-performance (~> 1.8) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.6.0) + standard-performance (1.8.0) lint_roller (~> 1.1) - rubocop-performance (~> 1.23.0) + rubocop-performance (~> 1.25.0) standard-rubocop-lts (1.0.10) rspec-block_is_expected (~> 1.0, >= 1.0.5) standard (>= 1.35.1, < 2) @@ -208,7 +216,7 @@ GEM version_gem (>= 1.1.4, < 3) stone_checksums (1.0.0) version_gem (>= 1.1.5, < 3) - stringio (3.1.5) + stringio (3.1.6) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.3.2) diff --git a/README.md b/README.md index 87bf215..d92c0f4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ [![Liberapay Patrons][⛳liberapay-img]][⛳liberapay] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] -[![Polar Shield][🖇polar-img]][🖇polar] +[![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon] @@ -92,7 +92,7 @@ If bundler is not being used to manage dependencies, install the gem by executin $ gem install version_gem -## Usage +## Basic Usage In the standard `bundle gem my_lib` code you get the following in `lib/my_lib/version.rb`: @@ -162,7 +162,55 @@ end Your `version.rb` file now abides the Ruby convention of directory / path matching the namespace / class! -### Zeitwerk +## Epoch Usage (Epoch Semantic Versioning) + +In the standard `bundle gem my_lib` code you get the following in `lib/my_lib/version.rb`: + +```ruby +module MyLib + VERSION = "0.1.0" +end +``` + +Change it to a nested `Version` namespace (the one implied by the path => namespace convention): + +```ruby +module MyLib + module Version + VERSION = "1024.3.8" + end +end +``` + +Now add the following near the top of the file the manages requiring external libraries. +Using the same example of `bundle gem my_lib`, this would be `lib/my_lib.rb`. + +```ruby +require "version_gem" +``` + +Then, add the following wherever you want in the same file (recommend the bottom). + +```ruby +MyLib::Version.class_eval do + extend VersionGem::Epoch +end +``` + +And now you have some version introspection methods available: + +```ruby +MyLib::Version.to_s # => "1024.3.8" +MyLib::Version.epoch # => 1 +MyLib::Version.major # => 24 +MyLib::Version.minor # => 3 +MyLib::Version.patch # => 8 +MyLib::Version.pre # => "" +MyLib::Version.to_a # => [1, 24, 3, 8] +MyLib::Version.to_h # => { epoch: 1, major: 24, minor: 3, patch: 8, pre: "" } +``` + +## Usage with Zeitwerk The pattern of `version.rb` breaking the ruby convention of directory / path matching the namespace / class is so entrenched that the `zeitwerk` library has a special carve-out for it. 🥺 @@ -312,11 +360,12 @@ Also see GitLab Contributors: [https://gitlab.com/oauth-xx/version_gem/-/graphs/ ## 📌 Versioning -This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver]. +This Library adheres to [![Epoch Semantic Versioning][📌semver-img]][📌semver]. Violations of this scheme should be reported as bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be immediately released that restores compatibility. -Breaking changes to the public API will only be introduced with new major versions. +Breaking changes to the public API, including dropping a supported platform (i.e. minor version of Ruby), will only be introduced with new major versions. +Epoch will only be bumped if there are dramatic changes, and that is not expected to happen ever. ### 📌 Is "Platform Support" part of the public API? @@ -464,12 +513,15 @@ or one of the others at the head of this README. [⛳liberapay]: https://liberapay.com/pboling/donate [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github [🖇sponsor]: https://github.com/sponsors/pboling -[🖇polar-img]: https://polar.sh/embed/seeks-funding-shield.svg?org=pboling +[🖇polar-img]: https://img.shields.io/badge/polar-donate-yellow.svg [🖇polar]: https://polar.sh/pboling -[🖇kofi-img]: https://img.shields.io/badge/buy_me_coffee-donate-yellow.svg +[🖇kofi-img]: https://img.shields.io/badge/a_more_different_coffee-✓-yellow.svg [🖇kofi]: https://ko-fi.com/O5O86SNP4 [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-yellow.svg [🖇patreon]: https://patreon.com/galtzo +[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff +[🖇buyme]: https://www.buymeacoffee.com/pboling +[🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-✓-yellow.svg?style=flat [💎ruby-2.2i]: https://img.shields.io/badge/Ruby-2.2-DF00CA?style=for-the-badge&logo=ruby&logoColor=white [💎ruby-2.3i]: https://img.shields.io/badge/Ruby-2.3-DF00CA?style=for-the-badge&logo=ruby&logoColor=white [💎ruby-2.4i]: https://img.shields.io/badge/Ruby-2.4-DF00CA?style=for-the-badge&logo=ruby&logoColor=white @@ -503,8 +555,8 @@ or one of the others at the head of this README. [🪇conduct]: CODE_OF_CONDUCT.md [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-4baaaa.svg [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint -[📌semver]: https://semver.org/spec/v2.0.0.html -[📌semver-img]: https://img.shields.io/badge/semver-2.0.0-FFDD67.svg?style=flat +[📌semver]: https://antfu.me/posts/epoch-semver +[📌semver-img]: https://img.shields.io/badge/epoch-semver-FFDD67.svg?style=flat [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html [📌changelog]: CHANGELOG.md @@ -522,6 +574,3 @@ or one of the others at the head of this README. [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-brightgreen.svg?style=flat [🚎yard-current]: http://rubydoc.info/gems/version_gem [🚎yard-head]: https://rubydoc.info/github/oauth-xx/version_gem/main -[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff -[🖇buyme]: https://www.buymeacoffee.com/pboling -[🖇buyme-small-img]: https://img.shields.io/badge/Buy--Me--A--Coffee-✓-brightgreen.svg?style=flat diff --git a/lib/version_gem.rb b/lib/version_gem.rb old mode 100644 new mode 100755 index 5749edf..6caacc7 --- a/lib/version_gem.rb +++ b/lib/version_gem.rb @@ -2,6 +2,7 @@ require_relative "version_gem/version" require_relative "version_gem/basic" +require_relative "version_gem/epoch" # Namespace of this library module VersionGem diff --git a/lib/version_gem/api.rb b/lib/version_gem/api.rb old mode 100644 new mode 100755 diff --git a/lib/version_gem/epoch.rb b/lib/version_gem/epoch.rb new file mode 100644 index 0000000..8db4472 --- /dev/null +++ b/lib/version_gem/epoch.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require_relative "error" +require_relative "api" + +module VersionGem + # Support for Epoch Semantic Versioning + # See: https://antfu.me/posts/epoch-semver + module Epoch + EPOCH_SIZE = 1_000 + + class << self + def extended(base) + raise Error, "VERSION must be defined before 'extend #{name}'" unless defined?(base::VERSION) + + base.extend(Api) + base.extend(OverloadApiForEpoch) + end + end + + module OverloadApiForEpoch + # *** OVERLOAD METHODS FROM API *** + # + # The epoch version + # + # @return [Integer] + def epoch + @epoch ||= _major / EPOCH_SIZE + end + + # The major version + # + # @return [Integer] + def major + @major ||= _major % EPOCH_SIZE + end + + # The version number as a hash + # + # @return [Hash] + def to_h + @to_h ||= { + epoch: epoch, + major: major, + minor: minor, + patch: patch, + pre: pre, + } + end + + # NOTE: This is not the same as _to_a, which returns an array of strings + # + # The version number as an array of cast values + # where epoch and major are derived from a single string: + # EPOCH * 1000 + MAJOR + # + # @return [Array<[Integer, String, NilClass]>] + def to_a + @to_a ||= [epoch, major, minor, patch, pre] + end + + private + + def _major + @_major ||= _to_a[0].to_i + end + end + end +end diff --git a/lib/version_gem/rspec.rb b/lib/version_gem/rspec.rb index 62a53d1..2fac74a 100644 --- a/lib/version_gem/rspec.rb +++ b/lib/version_gem/rspec.rb @@ -12,6 +12,12 @@ end end +RSpec::Matchers.define(:have_epoch_as_integer) do + match do |version_mod| + version_mod.epoch.is_a?(Integer) + end +end + RSpec::Matchers.define(:have_major_as_integer) do match do |version_mod| version_mod.major.is_a?(Integer) @@ -52,3 +58,21 @@ end end end + +RSpec.shared_examples_for("an Epoch Version module") do |version_mod| + it "is introspectable" do + aggregate_failures "introspectable api" do + expect(version_mod).is_a?(Module) + expect(version_mod).to(have_version_constant) + expect(version_mod).to(have_version_as_string) + expect(version_mod.to_s).to(be_a(String)) + expect(version_mod).to(have_epoch_as_integer) + expect(version_mod).to(have_major_as_integer) + expect(version_mod).to(have_minor_as_integer) + expect(version_mod).to(have_patch_as_integer) + expect(version_mod).to(have_pre_as_nil_or_string) + expect(version_mod.to_h.keys).to(match_array(%i[epoch major minor patch pre])) + expect(version_mod.to_a).to(be_a(Array)) + end + end +end diff --git a/lib/version_gem/version.rb b/lib/version_gem/version.rb index d4c4f9c..2942396 100644 --- a/lib/version_gem/version.rb +++ b/lib/version_gem/version.rb @@ -2,11 +2,13 @@ module VersionGem module Version - VERSION = "1.1.6" + VERSION = "1.1.7" # This would work in this gem, but not in external libraries, # because version files are loaded in Gemspecs before bundler # has a chance to load dependencies. # Instead, see lib/version_gem.rb for a solution that will work everywhere # extend VersionGem::Basic + # or + # extend VersionGem::Epoch end end diff --git a/spec/helpers/epoch_test.rb b/spec/helpers/epoch_test.rb new file mode 100644 index 0000000..4518c08 --- /dev/null +++ b/spec/helpers/epoch_test.rb @@ -0,0 +1,8 @@ +# If your version file will be required by a gemspec, do not do this, instead follow README.md#usage +module EpochTest + VERSION = "3012.34.56.pre-78" +end + +EpochTest.class_eval do + extend VersionGem::Epoch +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6087286..fc97a7b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,3 +31,4 @@ # RSpec Helpers which depend on gem internals require_relative "helpers/under_test" +require_relative "helpers/epoch_test" diff --git a/spec/version_gem/epoch_spec.rb b/spec/version_gem/epoch_spec.rb new file mode 100644 index 0000000..8f06a01 --- /dev/null +++ b/spec/version_gem/epoch_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# If your version file will be required by a gemspec, do not do this, instead follow README.md#usage + +RSpec.describe(VersionGem::Epoch) do + it "raises when no VERSION" do + expect do + Module.new do + extend VersionGem::Epoch + end + end.to(raise_error(VersionGem::Error, "VERSION must be defined before 'extend VersionGem::Epoch'")) + end + + context "when under test" do + subject(:epoch_test) { EpochTest } + + it_behaves_like "an Epoch Version module", EpochTest + + it "is greater than 0.1.0" do + expect(Gem::Version.new(epoch_test) > Gem::Version.new("0.1.0")).to(be(true)) + end + + it "epoch version is an integer" do + expect(epoch_test.epoch).to(eq(3)) + end + + it "major version is an integer" do + expect(epoch_test.major).to(eq(12)) + end + + it "minor version is an integer" do + expect(epoch_test.minor).to(eq(34)) + end + + it "patch version is an integer" do + expect(epoch_test.patch).to(eq(56)) + end + + it "pre version is an string" do + expect(epoch_test.pre).to(eq("pre-78")) + end + + it "returns a Hash" do + expect(epoch_test.to_h).to(eq(epoch: 3, major: 12, minor: 34, patch: 56, pre: "pre-78")) + end + + it "returns an Array" do + expect(epoch_test.to_a).to(eq([3, 12, 34, 56, "pre-78"])) + end + end +end