From 4d119c4d5b924ea257bfbe30e3b72b432bdf5182 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Sun, 11 Apr 2021 23:51:08 +0000 Subject: [PATCH 01/17] feat: remove the need for public use of const_missing --- lib/toggles/feature.rb | 10 ++++++++++ spec/toggles/feature/acceptance/type_spec.rb | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/toggles/feature.rb b/lib/toggles/feature.rb index c5aa0a9..7a346c8 100644 --- a/lib/toggles/feature.rb +++ b/lib/toggles/feature.rb @@ -21,4 +21,14 @@ def self.set_tree(tree) def self.const_missing(sym) @@tree.const_get(sym, inherit: false) end + + def self.enabled?(sym, **criteria) + @@tree.const_get(sym.to_s.split("_").map(&:capitalize).join(""), inherit: false) + &.enabled_for?(criteria) + end + + def self.disabled?(sym, **criteria) + @@tree.const_get(sym.to_s.split("_").map(&:capitalize).join(""), inherit: false) + &.disabled_for?(criteria) + end end diff --git a/spec/toggles/feature/acceptance/type_spec.rb b/spec/toggles/feature/acceptance/type_spec.rb index a5cd7be..c894e76 100644 --- a/spec/toggles/feature/acceptance/type_spec.rb +++ b/spec/toggles/feature/acceptance/type_spec.rb @@ -1,7 +1,12 @@ describe "Feature::Type" do specify do - expect(Feature::Type.enabled_for?(user_id: 1)).to eq true - expect(Feature::Type.enabled_for?(user_id: 25)).to eq false - expect(Feature::Type.enabled_for?(user_id: nil)).to eq false + aggregate_failures do + expect(Feature::Type.enabled_for?(user_id: 1)).to eq true + expect(Feature::Type.enabled_for?(user_id: 25)).to eq false + expect(Feature::Type.enabled_for?(user_id: nil)).to eq false + expect(Feature.enabled?(:type, user_id: 1)).to eq(true) + expect(Feature.disabled?(:type, user_id: 1)).to eq(false) + expect(Feature.enabled?(:type, user_id: 25)).to eq(false) + end end end From abf1cf1163f9418dce02aa794b08ab7c39f29b45 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 01:15:48 +0000 Subject: [PATCH 02/17] feat: deprecate const_missing feature lookups * Store the feature tree as nested hashes * Add simplified query methods * Continue to support constant lookups --- Gemfile | 5 ++++ features/nested_foo/bar_baz.yml | 4 +++ lib/toggles.rb | 52 +++++---------------------------- lib/toggles/feature.rb | 49 ++++++++++++++++++++++++++----- spec/spec_helper.rb | 2 ++ spec/toggles/init_spec.rb | 9 ++++-- toggles.gemspec | 3 -- 7 files changed, 65 insertions(+), 59 deletions(-) create mode 100644 features/nested_foo/bar_baz.yml diff --git a/Gemfile b/Gemfile index fa75df1..bea9a0f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,8 @@ source 'https://rubygems.org' gemspec + +group :test do + gem "pry" + gem "pry-nav" +end diff --git a/features/nested_foo/bar_baz.yml b/features/nested_foo/bar_baz.yml new file mode 100644 index 0000000..93cebb4 --- /dev/null +++ b/features/nested_foo/bar_baz.yml @@ -0,0 +1,4 @@ +foo: + id: + lt: + 5 diff --git a/lib/toggles.rb b/lib/toggles.rb index 5844104..84155fa 100644 --- a/lib/toggles.rb +++ b/lib/toggles.rb @@ -19,63 +19,25 @@ def configuration @configuration ||= Configuration.new end - # Dynamically create modules and classes within the `Feature` module based on - # the directory structure of `features`. - # - # For example if the `features` directory has the structure: - # - # features - # ├── thing - # | ├── one.yml - # | └── two.yml - # └── test.yml - # - # `Feature::Test`, `Feature::Thing::One`, `Feature::Thing::Two` would be - # available by default. - # def init return unless Dir.exists? configuration.features_dir - new_tree = Module.new - top_level = File.realpath(configuration.features_dir) top_level_p = Pathname.new(top_level) - Find.find(top_level) do |path| - previous = new_tree - abspath = path - path = Pathname.new(path).relative_path_from(top_level_p).to_s - if path.match(/\.ya?ml\Z/) - base = path.chomp(File.extname(path)).split("/") - if base.size > 1 - directories = base[0...-1] - filename = base[-1] - else - directories = [] - filename = base[0] - end - - directories.each do |directory| - module_name = directory.split("_").map(&:capitalize).join.to_sym - previous = if previous.constants.include? module_name - previous.const_get(module_name) - else - previous.const_set(module_name, Module.new) - end - end + Feature.features.clear - cls = Class.new(Feature::Base) do |c| - c.const_set(:PERMISSIONS, Feature::Permissions.new(abspath)) - end - - previous.const_set(filename.split("_").map(&:capitalize).join.to_sym, cls) + Dir[File.join(top_level, "**/*.{yaml,yml}")].each do |abspath| + path = Pathname.new(abspath).relative_path_from(top_level_p).to_s + features = path.split("/")[0..-2].inject(Feature.features) { |a,e| a[e.to_sym] ||= {} } + feature_key = File.basename(path, File.extname(path)).to_sym + features[feature_key] = Class.new(Feature::Base) do |c| + c.const_set(:PERMISSIONS, Feature::Permissions.new(abspath)) end end stbuf = File.stat(top_level) @stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime) - - Feature.set_tree(new_tree) end def reinit_if_changed diff --git a/lib/toggles/feature.rb b/lib/toggles/feature.rb index 7a346c8..115a3c7 100644 --- a/lib/toggles/feature.rb +++ b/lib/toggles/feature.rb @@ -12,23 +12,56 @@ module Feature or: Operation::Or, range: Operation::Range} - @@tree = Module.new + def self.features + @features ||= {} + end + + class Lookup + Error = Class.new(StandardError) do + attr_reader :sym + + def initialize(sym) + @sym = sym + super(sym.join('::')) + end + end + + def self.from(features, path) + Class.new do + class << self + attr_accessor :features + attr_accessor :path - def self.set_tree(tree) - @@tree = tree + def const_missing(sym) + subtree_or_feature = features.fetch( + sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, + ) + if subtree_or_feature.is_a?(Hash) + Lookup.from(subtree_or_feature, path + [sym]) + else + subtree_or_feature + end + rescue KeyError + raise Lookup::Error.new(path + [sym]) + end + end + end.tap do |lookup| + lookup.features = features + lookup.path = path + end + end end + # @deprecated This is an abuse of lazy dispatch that creates cryptic errors def self.const_missing(sym) - @@tree.const_get(sym, inherit: false) + Lookup.from(features, [:Feature]).const_missing(sym) end def self.enabled?(sym, **criteria) - @@tree.const_get(sym.to_s.split("_").map(&:capitalize).join(""), inherit: false) - &.enabled_for?(criteria) + features.fetch(sym).enabled_for?(criteria) end def self.disabled?(sym, **criteria) - @@tree.const_get(sym.to_s.split("_").map(&:capitalize).join(""), inherit: false) - &.disabled_for?(criteria) + features.fetch(sym).disabled_for?(criteria) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8615567..a228de5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,8 @@ require "toggles" +Bundler.require(:test) + RSpec.configure do |config| config.order = "random" diff --git a/spec/toggles/init_spec.rb b/spec/toggles/init_spec.rb index af5d81a..efd3c8a 100644 --- a/spec/toggles/init_spec.rb +++ b/spec/toggles/init_spec.rb @@ -17,7 +17,7 @@ Toggles.configure do |c| c.features_dir = temp_dir end - + expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true) expect(Feature::Bar::Users.enabled_for?(id: 3)).to eq(true) end @@ -36,7 +36,7 @@ Toggles.configure do |c| c.features_dir = temp_dir end - + expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true) expect(Feature::Foo::Children.enabled_for?(id: 1)).to eq(true) @@ -49,7 +49,10 @@ Toggles.init expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(false) - expect { Feature::Foo::Children.enabled_for?(id: 1) }.to raise_error(NameError) + expect { Feature::Bar::Children.enabled_for?(id: 1) } + .to raise_error(Feature::Lookup::Error, 'Feature::Bar') + expect { Feature::Foo::Children.enabled_for?(id: 1) } + .to raise_error(Feature::Lookup::Error, 'Feature::Foo::Children') end end diff --git a/toggles.gemspec b/toggles.gemspec index 02c3da2..5624dd1 100644 --- a/toggles.gemspec +++ b/toggles.gemspec @@ -16,9 +16,6 @@ Gem::Specification.new do |s| EOF s.add_development_dependency "bundler" - s.add_development_dependency "pry" - s.add_development_dependency "pry-nav" - s.add_development_dependency "pry-remote" s.add_development_dependency "rake" s.add_development_dependency "rspec" s.add_development_dependency "rspec-its" From 60d4e3e67ffa8f7703b96fcd4b1eb469190df634 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 01:39:16 +0000 Subject: [PATCH 03/17] refactor: common error class, constant vs path lookup errors and tests --- features/nested_foo/bar_baz.yml | 7 ++--- lib/toggles/feature.rb | 29 ++++++++++++++------ spec/toggles/feature/acceptance/type_spec.rb | 13 ++++++++- spec/toggles/init_spec.rb | 4 +-- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/features/nested_foo/bar_baz.yml b/features/nested_foo/bar_baz.yml index 93cebb4..a798e1e 100644 --- a/features/nested_foo/bar_baz.yml +++ b/features/nested_foo/bar_baz.yml @@ -1,4 +1,3 @@ -foo: - id: - lt: - 5 +id: + lt: + 5 diff --git a/lib/toggles/feature.rb b/lib/toggles/feature.rb index 115a3c7..b3d548a 100644 --- a/lib/toggles/feature.rb +++ b/lib/toggles/feature.rb @@ -12,12 +12,15 @@ module Feature or: Operation::Or, range: Operation::Range} + Error = Class.new(StandardError) + Unknown = Class.new(Error) + def self.features @features ||= {} end - class Lookup - Error = Class.new(StandardError) do + class ConstantLookup + Error = Class.new(Feature::Error) do attr_reader :sym def initialize(sym) @@ -37,12 +40,12 @@ def const_missing(sym) sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, ) if subtree_or_feature.is_a?(Hash) - Lookup.from(subtree_or_feature, path + [sym]) + ConstantLookup.from(subtree_or_feature, path + [sym]) else subtree_or_feature end rescue KeyError - raise Lookup::Error.new(path + [sym]) + raise ConstantLookup::Error.new(path + [sym]) end end end.tap do |lookup| @@ -54,14 +57,22 @@ def const_missing(sym) # @deprecated This is an abuse of lazy dispatch that creates cryptic errors def self.const_missing(sym) - Lookup.from(features, [:Feature]).const_missing(sym) + ConstantLookup.from(features, [:Feature]).const_missing(sym) end - def self.enabled?(sym, **criteria) - features.fetch(sym).enabled_for?(criteria) + def self.enabled?(*sym, **criteria) + sym + .inject(features) { |a, e| a.fetch(e) } + .enabled_for?(criteria) + rescue KeyError + raise Unknown, sym.inspect end - def self.disabled?(sym, **criteria) - features.fetch(sym).disabled_for?(criteria) + def self.disabled?(*sym, **criteria) + sym + .inject(features) { |a, e| a.fetch(e) } + .disabled_for?(criteria) + rescue KeyError + raise Unknown, sym.inspect end end diff --git a/spec/toggles/feature/acceptance/type_spec.rb b/spec/toggles/feature/acceptance/type_spec.rb index c894e76..bfb1793 100644 --- a/spec/toggles/feature/acceptance/type_spec.rb +++ b/spec/toggles/feature/acceptance/type_spec.rb @@ -1,12 +1,23 @@ describe "Feature::Type" do - specify do + specify 'deprecated' do aggregate_failures do expect(Feature::Type.enabled_for?(user_id: 1)).to eq true expect(Feature::Type.enabled_for?(user_id: 25)).to eq false expect(Feature::Type.enabled_for?(user_id: nil)).to eq false + end + end + + specify do + aggregate_failures do expect(Feature.enabled?(:type, user_id: 1)).to eq(true) expect(Feature.disabled?(:type, user_id: 1)).to eq(false) expect(Feature.enabled?(:type, user_id: 25)).to eq(false) + expect(Feature.enabled?(:nested_foo, :bar_baz, id: 25)).to eq(false) + expect(Feature.enabled?(:nested_foo, :bar_baz, id: 1)).to eq(true) + expect(Feature.disabled?(:type, user_id: 25)).to eq(true) + expect(Feature.disabled?(:nested_foo, :bar_baz, id: 25)).to eq(true) + expect(Feature.disabled?(:nested_foo, :bar_baz, id: 1)).to eq(false) + expect { Feature.disabled?(:nested_foo, :bar_boz, id: 1) }.to raise_error(Feature::Unknown) end end end diff --git a/spec/toggles/init_spec.rb b/spec/toggles/init_spec.rb index efd3c8a..14857a8 100644 --- a/spec/toggles/init_spec.rb +++ b/spec/toggles/init_spec.rb @@ -50,9 +50,9 @@ expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(false) expect { Feature::Bar::Children.enabled_for?(id: 1) } - .to raise_error(Feature::Lookup::Error, 'Feature::Bar') + .to raise_error(Feature::ConstantLookup::Error, 'Feature::Bar') expect { Feature::Foo::Children.enabled_for?(id: 1) } - .to raise_error(Feature::Lookup::Error, 'Feature::Foo::Children') + .to raise_error(Feature::ConstantLookup::Error, 'Feature::Foo::Children') end end From 5a5e1156e6bcee17ed4343ed6e21120e99711329 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 01:43:27 +0000 Subject: [PATCH 04/17] doc(README): remove references to constant generation --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 57ddff5..02ef5aa 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,6 @@ features └── test.yml ``` -The classes `Feature::Test`, `Feature::Thing::One` and `Feature::Thing::Two` will be available for use within -your application. - You can call the `Toggles.init` method to force re-parsing the configuration and re-initializing all Features structures at any time. The `Toggles.reinit_if_necessary` method is a convenience helper which will only re-initialize of the top-level features directory has changed. Note that, in general, this will only detect @@ -65,13 +62,13 @@ user: Check if the feature is enabled or disabled: ```ruby -Feature::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 12345)) # true -Feature::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 54321)) # true -Feature::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 7)) # false +Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # true +Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # true +Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # false -Feature::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 12345)) # false -Feature::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 54321)) # false -Feature::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 7)) # true +Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # false +Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # false +Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # true ``` ## License From 9e0428d02fc8cc6e6a827e7cb58bf94fccbb3ed2 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 01:55:38 +0000 Subject: [PATCH 05/17] chore(ci): 2.5, 2.6 and 2.7 ci check only --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e49532..ac0f58a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - rubyversion: ['2.4', '2.5', '2.6', '2.7', '3.0'] + rubyversion: ['2.5', '2.6', '2.7'] steps: - uses: actions/checkout@v2 - name: set up ruby From 0c22d714214447da42f9b04043c7d18ba6be2b5d Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 02:00:46 +0000 Subject: [PATCH 06/17] doc(README): correct message reference --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 02ef5aa..15f47a3 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ user: Check if the feature is enabled or disabled: ```ruby -Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # true -Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # true -Feature.enabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # false +Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # true +Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # true +Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # false -Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # false -Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # false -Feature.disabled_for?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # true +Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # false +Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # false +Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # true ``` ## License From 2c7a34ccf6bd174cf88178811105e1ac0c869380 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 16:10:04 +0000 Subject: [PATCH 07/17] refactor: extract constant_lookup class and define resolver once --- lib/toggles/constant_lookup.rb | 37 +++++++++++++++++++++++++++++++++ lib/toggles/feature.rb | 38 ++-------------------------------- 2 files changed, 39 insertions(+), 36 deletions(-) create mode 100644 lib/toggles/constant_lookup.rb diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb new file mode 100644 index 0000000..f0975d5 --- /dev/null +++ b/lib/toggles/constant_lookup.rb @@ -0,0 +1,37 @@ +class Feature::ConstantLookup + Error = Class.new(Feature::Error) do + attr_reader :sym + + def initialize(sym) + @sym = sym + super(sym.join('::')) + end + end + + Resolver = Class.new do + class << self + attr_accessor :features + attr_accessor :path + + def const_missing(sym) + subtree_or_feature = features.fetch( + sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, + ) + if subtree_or_feature.is_a?(Hash) + Feature::ConstantLookup.from(subtree_or_feature, path + [sym]) + else + subtree_or_feature + end + rescue KeyError + raise Error.new(path + [sym]) + end + end + end + + def self.from(features, path) + Resolver.tap do |resolver| + resolver.features = features + resolver.path = path + end + end +end diff --git a/lib/toggles/feature.rb b/lib/toggles/feature.rb index b3d548a..06ff318 100644 --- a/lib/toggles/feature.rb +++ b/lib/toggles/feature.rb @@ -19,42 +19,6 @@ def self.features @features ||= {} end - class ConstantLookup - Error = Class.new(Feature::Error) do - attr_reader :sym - - def initialize(sym) - @sym = sym - super(sym.join('::')) - end - end - - def self.from(features, path) - Class.new do - class << self - attr_accessor :features - attr_accessor :path - - def const_missing(sym) - subtree_or_feature = features.fetch( - sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, - ) - if subtree_or_feature.is_a?(Hash) - ConstantLookup.from(subtree_or_feature, path + [sym]) - else - subtree_or_feature - end - rescue KeyError - raise ConstantLookup::Error.new(path + [sym]) - end - end - end.tap do |lookup| - lookup.features = features - lookup.path = path - end - end - end - # @deprecated This is an abuse of lazy dispatch that creates cryptic errors def self.const_missing(sym) ConstantLookup.from(features, [:Feature]).const_missing(sym) @@ -76,3 +40,5 @@ def self.disabled?(*sym, **criteria) raise Unknown, sym.inspect end end + +require 'toggles/constant_lookup' From ffb67641d460d0c4173d85876ae3f3346447bb2f Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 16:44:10 +0000 Subject: [PATCH 08/17] fix: cut a new class everytime to be thread safe --- lib/toggles/constant_lookup.rb | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb index f0975d5..6e2f736 100644 --- a/lib/toggles/constant_lookup.rb +++ b/lib/toggles/constant_lookup.rb @@ -8,28 +8,27 @@ def initialize(sym) end end - Resolver = Class.new do - class << self - attr_accessor :features - attr_accessor :path - def const_missing(sym) - subtree_or_feature = features.fetch( - sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, - ) - if subtree_or_feature.is_a?(Hash) - Feature::ConstantLookup.from(subtree_or_feature, path + [sym]) - else - subtree_or_feature + def self.from(features, path) + Class.new { + class << self + attr_accessor :features + attr_accessor :path + + def const_missing(sym) + subtree_or_feature = features.fetch( + sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, + ) + if subtree_or_feature.is_a?(Hash) + Feature::ConstantLookup.from(subtree_or_feature, path + [sym]) + else + subtree_or_feature + end + rescue KeyError + raise Error.new(path + [sym]) end - rescue KeyError - raise Error.new(path + [sym]) end - end - end - - def self.from(features, path) - Resolver.tap do |resolver| + }.tap do |resolver| resolver.features = features resolver.path = path end From c68e4b60d9a3900358db4cd04094eb124ce25e70 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 12 Apr 2021 17:44:07 +0000 Subject: [PATCH 09/17] refactor: simplify sym to constant lookup transform function --- lib/toggles/constant_lookup.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb index 6e2f736..eb5110d 100644 --- a/lib/toggles/constant_lookup.rb +++ b/lib/toggles/constant_lookup.rb @@ -17,7 +17,7 @@ class << self def const_missing(sym) subtree_or_feature = features.fetch( - sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars[0] + "_" + s.chars[1] }.downcase.to_sym, + sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars.join('_') }.downcase.to_sym, ) if subtree_or_feature.is_a?(Hash) Feature::ConstantLookup.from(subtree_or_feature, path + [sym]) From 5d347d1bed31112a229c2142652a40867349ad3b Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Wed, 14 Apr 2021 03:04:00 +0000 Subject: [PATCH 10/17] doc: context for less obvious functions --- lib/toggles/constant_lookup.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb index eb5110d..a58ba82 100644 --- a/lib/toggles/constant_lookup.rb +++ b/lib/toggles/constant_lookup.rb @@ -9,6 +9,16 @@ def initialize(sym) end + # Return a tree walker that translates Module#const_missing(sym) into the next child node + # + # So Features::Cat::BearDog walks as: + # * next_features = Feature.features # root + # * const_missing(:Cat) => next_features = next_features['cat'] + # * const_missing(:BearDog) => next_features['bear_dog'] + # + # Defined at Toggles.features_dir + "/cat/bear_dog.yaml" + # + # @raise [Error] if constant cannot be resoled def self.from(features, path) Class.new { class << self @@ -17,6 +27,7 @@ class << self def const_missing(sym) subtree_or_feature = features.fetch( + # translate class name into path part i.e :BearDog #=> 'bear_dog' sym.to_s.gsub(/([a-z])([A-Z])/) { |s| s.chars.join('_') }.downcase.to_sym, ) if subtree_or_feature.is_a?(Hash) From 48e0107057976a40e1a02d9ce17a9c02f5cb7c6d Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Wed, 11 Aug 2021 19:25:48 +0000 Subject: [PATCH 11/17] refactor: simplify #disabled? --- lib/toggles/feature.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/toggles/feature.rb b/lib/toggles/feature.rb index 06ff318..92f54ca 100644 --- a/lib/toggles/feature.rb +++ b/lib/toggles/feature.rb @@ -33,11 +33,7 @@ def self.enabled?(*sym, **criteria) end def self.disabled?(*sym, **criteria) - sym - .inject(features) { |a, e| a.fetch(e) } - .disabled_for?(criteria) - rescue KeyError - raise Unknown, sym.inspect + !enabled?(*sym, **criteria) end end From 8c37edb06e53ecc47ba944df2dcb01a1627445b0 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Wed, 11 Aug 2021 19:26:15 +0000 Subject: [PATCH 12/17] doc: typo --- lib/toggles/constant_lookup.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb index a58ba82..f28bbdb 100644 --- a/lib/toggles/constant_lookup.rb +++ b/lib/toggles/constant_lookup.rb @@ -18,7 +18,7 @@ def initialize(sym) # # Defined at Toggles.features_dir + "/cat/bear_dog.yaml" # - # @raise [Error] if constant cannot be resoled + # @raise [Error] if constant cannot be resolved def self.from(features, path) Class.new { class << self From 21deac3859e4cd8a640c9fae320481bf517edde5 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Thu, 12 Aug 2021 20:36:57 +0000 Subject: [PATCH 13/17] refactor: inherit from NameError to keep existing compatibility --- lib/toggles/constant_lookup.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/toggles/constant_lookup.rb b/lib/toggles/constant_lookup.rb index f28bbdb..8107577 100644 --- a/lib/toggles/constant_lookup.rb +++ b/lib/toggles/constant_lookup.rb @@ -1,5 +1,5 @@ class Feature::ConstantLookup - Error = Class.new(Feature::Error) do + Error = Class.new(NameError) do attr_reader :sym def initialize(sym) From 76dd369c5edffe55a71548771bd9bd5e3e794c1e Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 16 Aug 2021 18:51:22 +0000 Subject: [PATCH 14/17] chore: revert ci workflow changes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac0f58a..4e49532 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - rubyversion: ['2.5', '2.6', '2.7'] + rubyversion: ['2.4', '2.5', '2.6', '2.7', '3.0'] steps: - uses: actions/checkout@v2 - name: set up ruby From 2d2b1fea6afa77c11092f98dccba47606dffcc00 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 16 Aug 2021 13:05:44 -0700 Subject: [PATCH 15/17] fix: require set --- lib/toggles.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/toggles.rb b/lib/toggles.rb index 84155fa..0dda0c9 100644 --- a/lib/toggles.rb +++ b/lib/toggles.rb @@ -1,5 +1,6 @@ require "find" require "pathname" +require "set" require "toggles/configuration" require "toggles/feature" From cc9de6424ff276795047697ea81c0edb0481b2c6 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 16 Aug 2021 13:09:33 -0700 Subject: [PATCH 16/17] chore: create a version file --- lib/toggles.rb | 2 ++ lib/toggles/version.rb | 5 +++++ toggles.gemspec | 32 +++++++++++++++++++------------- 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 lib/toggles/version.rb diff --git a/lib/toggles.rb b/lib/toggles.rb index 0dda0c9..d7302d9 100644 --- a/lib/toggles.rb +++ b/lib/toggles.rb @@ -2,6 +2,8 @@ require "pathname" require "set" +require "toggles/version" + require "toggles/configuration" require "toggles/feature" diff --git a/lib/toggles/version.rb b/lib/toggles/version.rb new file mode 100644 index 0000000..b3008c9 --- /dev/null +++ b/lib/toggles/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Toggles + VERSION = '0.2.1' +end diff --git a/toggles.gemspec b/toggles.gemspec index 5624dd1..b26c85a 100644 --- a/toggles.gemspec +++ b/toggles.gemspec @@ -1,13 +1,19 @@ +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'toggles/version' + Gem::Specification.new do |s| - s.name = "toggles" - s.version = "0.1.2" - s.authors = ["Andrew Tribone", "James Brown"] - s.summary = "YAML backed feature toggles" - s.email = "oss@easypost.com" - s.homepage = "https://github.com/EasyPost/toggles" - s.license = "ISC" + s.name = 'toggles' + s.version = Toggles::VERSION + s.authors = ['Andrew Tribone', 'James Brown', 'Josh Lane'] + s.summary = 'YAML backed feature toggles' + s.email = 'oss@easypost.com' + s.homepage = 'https://github.com/EasyPost/toggles' + s.license = 'ISC' s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) - s.test_files = s.files.grep(/^(spec)\//) + s.test_files = s.files.grep(%r{^(spec)/}) s.description = <<-EOF YAML-backed implementation of the feature flags pattern. Build a hierarchy of features in YAML files in the filesystem, apply various @@ -15,9 +21,9 @@ Gem::Specification.new do |s| check whether a given feature should be applied. EOF - s.add_development_dependency "bundler" - s.add_development_dependency "rake" - s.add_development_dependency "rspec" - s.add_development_dependency "rspec-its" - s.add_development_dependency "rspec-temp_dir" + s.add_development_dependency 'bundler' + s.add_development_dependency 'rake' + s.add_development_dependency 'rspec' + s.add_development_dependency 'rspec-its' + s.add_development_dependency 'rspec-temp_dir' end From 6b4fac8fc759701deb6fd6e082c360d0316e7a87 Mon Sep 17 00:00:00 2001 From: Joshua Lane Date: Mon, 16 Aug 2021 13:10:15 -0700 Subject: [PATCH 17/17] Bump toggles to 0.3.0 --- lib/toggles/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/toggles/version.rb b/lib/toggles/version.rb index b3008c9..b4a5875 100644 --- a/lib/toggles/version.rb +++ b/lib/toggles/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Toggles - VERSION = '0.2.1' + VERSION = '0.3.0' end