gem_enforcer 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 259683929871f4f3ba6aabed1d4730837860fd2fe0c983f19baf735da1be2edc
4
- data.tar.gz: 962e62bd2f891d48b4abf50efe0e44fbb4d43938ef745189c075c5753ca86faf
3
+ metadata.gz: 743105e56589c3d27eab2fae8e761edde554f552922c43fc474a93be9ed0cfe8
4
+ data.tar.gz: 0e116051836cfe75a48b444483956e84b705c883a5552e8eda842d8415885f9d
5
5
  SHA512:
6
- metadata.gz: 86b8b4f2755007c7f1973c2dc844066b4060cc0eb1729906cdabcbd3ad91d17eed1274be1bc3c26a01d6c5459ed660cd547657409b25b844b45d7dc75919e8e0
7
- data.tar.gz: debf1f1abd7668a5a36413ee1156b69bba9b0a8d0cb0f4bfd16801c5db74cb58d66e3175a5b1e92d21d1df216a8cebe7da9a0b3db996152aaafef450304d2a4d
6
+ metadata.gz: 5162dfa82d5e76f7dd6045b2595fb15e913eec2b98e62e60fa4d14cd5db8be9fb565e0b669697485c0a5be84a1fad5cffcabe584f39873309720d9386d9d8965
7
+ data.tar.gz: fee03c5789429dd03e046b2b9d806e81a3503b1e97117e5ee5bfe559400cc6dbb0a1c436a334ffc5888f6c3acc6e0b2d4140fe42bbe633b32eeaa9b58d16606c
data/Gemfile CHANGED
@@ -4,8 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- gem "faraday", "< 2.6"
8
- gem 'activesupport'
9
7
  gem 'faker'
10
8
  gem 'pry'
11
9
  gem 'rspec', '~> 3.0'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gem_enforcer (0.0.1)
4
+ gem_enforcer (0.1.0)
5
5
  class_composer (>= 1.0)
6
6
  faraday
7
7
  octokit
@@ -9,28 +9,14 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (7.1.4)
13
- base64
14
- bigdecimal
15
- concurrent-ruby (~> 1.0, >= 1.0.2)
16
- connection_pool (>= 2.2.5)
17
- drb
18
- i18n (>= 1.6, < 2)
19
- minitest (>= 5.1)
20
- mutex_m
21
- tzinfo (~> 2.0)
22
12
  addressable (2.8.7)
23
13
  public_suffix (>= 2.0.2, < 7.0)
24
- base64 (0.2.0)
25
- bigdecimal (3.1.8)
26
14
  byebug (11.1.3)
27
15
  class_composer (1.0.2)
28
16
  coderay (1.1.3)
29
17
  concurrent-ruby (1.3.4)
30
- connection_pool (2.4.1)
31
18
  diff-lcs (1.5.1)
32
19
  docile (1.4.1)
33
- drb (2.2.1)
34
20
  faker (3.4.2)
35
21
  i18n (>= 1.8.11, < 2)
36
22
  faraday (2.5.2)
@@ -40,8 +26,6 @@ GEM
40
26
  i18n (1.14.5)
41
27
  concurrent-ruby (~> 1.0)
42
28
  method_source (1.1.0)
43
- minitest (5.25.1)
44
- mutex_m (0.2.0)
45
29
  octokit (9.1.0)
46
30
  faraday (>= 1, < 3)
47
31
  sawyer (~> 0.9)
@@ -78,17 +62,13 @@ GEM
78
62
  simplecov_json_formatter (~> 0.1)
79
63
  simplecov-html (0.13.0)
80
64
  simplecov_json_formatter (0.1.4)
81
- tzinfo (2.0.6)
82
- concurrent-ruby (~> 1.0)
83
65
 
84
66
  PLATFORMS
85
67
  aarch64-linux
86
68
  ruby
87
69
 
88
70
  DEPENDENCIES
89
- activesupport
90
71
  faker
91
- faraday (< 2.6)
92
72
  gem_enforcer!
93
73
  pry
94
74
  pry-byebug
data/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # GemEnforcer
2
2
 
3
- `GemEnforcer` is
3
+ `GemEnforcer` is intended to ensure that your ruby scripts services or gems are using an acceptable version of specific gems. In the event that the gem is out of compliance, GemEnforcer can exit or raise an error before continuing.
4
+
5
+ ## Inspiration
6
+
7
+ I build a lot of scripts that live locally and on other developers laptops. It is hard to ensure that they know when a new version of a critical gem has been realeased. With GemEnforcer, after upgrading, I can enforce the developer is using:
8
+ - One of the 3 most recently released versions of Sidekiq
9
+ - Within 3 minor versions of the most recently released Major version
10
+ - The most recent patch vesion of a current minor version
11
+ - And so many more combinations
12
+
4
13
 
5
14
  ## Installation
6
15
 
@@ -18,16 +27,54 @@ Or install it yourself as:
18
27
 
19
28
  $ gem install gem_enforcer
20
29
 
30
+ ### Create a Configuration File
31
+
32
+ [Check out Configuration examples](examples)
33
+
34
+ ### Configure the Gem
35
+
36
+ Prior to running the validation, you can set custom configurations to tailor the GemEnforcement experience
37
+
38
+ ```ruby
39
+ GemEnforcer.configuration do |config|
40
+ # TO use Git Tags, the access token must be provided
41
+ # As a default it will check ENV["GITHUB_TOKEN"] or ENV["BUNDLE_GITHUB__COM"] if it is there
42
+ # If not, it must be provided to use git tag
43
+ config.github_access_token = "my_access_token"
44
+
45
+ config.yml_config_path = "The path to your config file"
46
+ config.logger = MyLoggerClass #TTY::Logger or Logger classes allowed
47
+ end
48
+ ```
49
+
50
+ ### Run Validation
51
+
52
+ Validations can get run anywhere in your code. Suggested for it to run early on during boot process
53
+
54
+ ```ruby
55
+ # Run configuration validations
56
+ unless GemEnforcer::Setup.validate_yml!
57
+ # There are errors in the configuration
58
+ puts GemEnforcer::Setup.errors
59
+ end
60
+
61
+ # Run the validations based on the config file
62
+ # If validate_yml! failed, this will raise an error!
63
+ GemEnforcer::Setup.run_validations!
64
+ ```
21
65
 
22
66
  ## Development
23
67
 
24
- This gem can be developed against local machine or while using docker. Simpleified Docker commands can be found in the `Makefile` or execute `make help`
68
+ This Gem is very close to 100% test coverage. To understand the inner workings, I would start with the test cases
69
+
70
+ You can run this gem using docker with make commands (`make bash`) or just on your local machine running at least Ruby 3.2.
71
+
25
72
 
26
73
  ## Contributing
27
74
 
28
75
  This gem welcomes contribution.
29
76
 
30
77
  Bug reports and pull requests are welcome on GitHub at
31
- https://github.com/matt-taylor/json_schematize.
78
+ https://github.com/matt-taylor/gem_enforcer.
32
79
 
33
80
 
@@ -0,0 +1,19 @@
1
+ # Examples
2
+
3
+ ## [Single Validation Example](git_tag.yml)
4
+
5
+ This is the most basic example. GemEnforcment provides some sane defaults for you to get going quickly. With minimal information, you can start enforcing gem versioning in your script, gem, or application!
6
+
7
+ ## [Enforcement with Multiple Behaviors Example](multiple_behavior.yml)
8
+
9
+ Multiple behaviors can be super helpful when you want to send a warning before the nuclear option of exiting or raising an error. Using multiple behaviors you can stack the experience and layer multiple warnings on when the Enforcment becomes more forceful
10
+
11
+ (Also shows a basic example of how a single config can support multiple gem validations)
12
+
13
+ ## [Git Tag with SemVer Example](git_tag.yml)
14
+
15
+ ### Git Tag
16
+ Git Tags allow you to query the version list from git tags. This can help the gem you care about is not in a gem source but rather just in git
17
+
18
+ ### SemVer Version Enforcement
19
+ SemVer versioning enforcment allows you to tailor the Gem Enforcement. Based on the Major, Minor, and/or Patch version of the current gem, you can set individual requirements to meet your needs
@@ -0,0 +1,26 @@
1
+ # This config file will ensure rails_base gem is up to date based on SemVer specifics
2
+ # NOTE: Usage of git is a bit more expensive and will consume Github API get operations that are counted towards rate limit
3
+ # Major:
4
+ # Major version is set to 0. This will enforce that the gem is up to date with the latest major.
5
+ # If the current major version does not equal what is released, failure behavior is triggered
6
+ #
7
+ # Patch:
8
+ # Given the current versions major.minor, the patch version is expected to be within the most 2 recent releases
9
+ # If not, it will trigger a failure
10
+ #
11
+ # Notice that minor is missing. This means it does not matter what minor version the current gem is on only that it is
12
+ # on the most recent major version and within 2 of the most recent patches released for the given minor version
13
+ #
14
+ # On Failure:
15
+ # Using log level Error, output the default message and then raise an error
16
+ ---
17
+ gems:
18
+ rails_base: # Enforce the gem rails_base
19
+ git: matt-taylor # Using git tags where the owner is matt-taylor and the gem name is rails_base
20
+ behaviors: # Array of different behavior -- Order matters here
21
+ - version_enforce:
22
+ major: 0 # Must be on at least the most recent major version
23
+ patch: 2 # For the given current major.minor version, must be within at least 2 patches
24
+ on_failure:
25
+ log_level: error # Log Level to output (This can be a custom log level based on your logger)
26
+ behavior: raise
@@ -0,0 +1,40 @@
1
+ # This config file will ensure Rails gem is up to date with the most recent release
2
+ # It uses the default rubygems.org ruby server to query versions against
3
+ # It has two behaviors
4
+ # Behavior 1:
5
+ # => Must be within the last 3 releases
6
+ # => When not within last 3 releases, it will log message and exit immediately
7
+ # Behavior 2:
8
+ # => Must be within the last 2 releases
9
+ # => When not within last 2 releases, it will log message and continue
10
+ # => Behavior is seen as a warning
11
+ #
12
+ # With multiple behaviors, order is important. If order was switched, if more than 3 releases behind,
13
+ # both messages would be shown to user instead of just one
14
+ # => Log Message via ERROR
15
+ # => Do nothing
16
+
17
+ # Config can support multiple gems!!
18
+ ---
19
+ gems:
20
+ rails: # Enforce the gem Rails
21
+ server: true # Use the default gem server (https://rubygems.org)
22
+ behaviors: # Array of different behavior -- Order matters here
23
+ - version_enforce:
24
+ releases: 3 # Behavior 1 will trigger when the current version is greater than the 3rd most recent and beyond
25
+ on_failure:
26
+ log_level: error # Log Level to output (This can be a custom log level based on your logger)
27
+ # message: Custom Message to output # There is a default message and this is optional but takes precedence
28
+ behavior: exit # When behavior is triggered, exit the application immediately
29
+ - version_enforce:
30
+ releases: 2 # Behavior 2 will trigger when the current version is 2nd most recent and beyond
31
+ on_failure:
32
+ log_level: warn # Log Level to output (This can be a custom log level based on your logger)
33
+ # Message can have interpolated strings: `version` => current version, `versions_behind` => versions behind, `max_version` => most recent version
34
+ message: "You are running rails v%{version} which is %{versions_behind} versions behind. Please consider updating"
35
+
36
+ sidekiq:
37
+ server: https://rubygems.org # Input your custom server here
38
+ behaviors:
39
+ - version_enforce:
40
+ insync: true # Behavior 1 triggered as soon as version is not the most recent
@@ -0,0 +1,14 @@
1
+ # This config file will ensure Sidekiq gem is up to date with the most recent release
2
+ # When this is not met, it will use the default failure behavior
3
+ # => Log Message via ERROR
4
+ # => Do nothing
5
+ ---
6
+ gems:
7
+ sidekiq:
8
+ server: https://rubygems.org # Input your custom server here
9
+ behaviors:
10
+ - version_enforce:
11
+ insync: true # Behavior 1 triggered as soon as version is not the most recent
12
+ # on_failure: This is the default on_failure behavior when none is provided
13
+ # log_level: error
14
+ # behavior: # none Logs message and returns
data/gem_enforcer.gemspec CHANGED
@@ -36,5 +36,4 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "pry-byebug"
37
37
  spec.add_development_dependency "rake", "~> 12.0"
38
38
  spec.add_development_dependency "rspec", "~> 3.0"
39
- spec.add_development_dependency "simplecov", "~> 0.17.0"
40
39
  end
data/gem_enforcer.yml CHANGED
@@ -4,56 +4,56 @@ invalid_config:
4
4
  behavior: exit
5
5
  gems:
6
6
  rails:
7
- on_failure:
8
- log_level: warn # on failure, log a warn message
9
- behavior: exit
10
- version_threshold:
11
- minor: 3 # within the last 3 minor releases
7
+ behaviors:
8
+ - on_failure:
9
+ log_level: warn # on failure, log a warn message
10
+ behavior: exit
11
+ version_enforce:
12
+ minor: 3 # within the last 3 minor releases
12
13
  server: true # when set to true, defaults to https://rubygems.org
13
14
 
14
15
  shoryuken:
15
- on_failure:
16
- log_level: info # on failure, log a info message
17
- behavior: skip
18
- version_threshold:
19
- # Must be within the most current version released
20
- # If above is true, must be within the last 2 minor releases
21
- # If above is true, must be within the last 3 patches released
22
- major: 0 # within the current major version
23
- minor: 2 # within the last 3 minor releases
24
- patch: 3 # within the last 3 patches released
16
+ behaviors:
17
+ - on_failure:
18
+ log_level: info # on failure, log a info message
19
+ version_enforce:
20
+ # Must be within the most current version released
21
+ # If above is true, must be within the last 2 minor releases
22
+ # If above is true, must be within the last 3 patches released
23
+ major: 0 # within the current major version
24
+ minor: 2 # within the last 3 minor releases
25
+ patch: 3 # within the last 3 patches released
25
26
  server: true # when set to true, defaults to https://rubygems.org
26
27
 
27
28
  redis:
28
- on_failure:
29
- log_level: error
30
- behavior: raise # on failure, log error and exit(1)
31
- enforce_insync: true # Ensure gem is up to date
32
- server:
33
- source: https://rubygems.org
29
+ behaviors:
30
+ - on_failure:
31
+ log_level: error
32
+ behavior: raise # on failure, log error and exit(1)
33
+ version_enforce:
34
+ insync: true # Ensure gem is up to date
35
+ server: https://rubygems.org
34
36
 
35
37
  faraday:
36
- on_failure:
37
- log_level: error
38
- behavior: exit # on failure, log error and exit(1)
39
- enforce_insync: true
40
- # version_threshold:
41
-
42
- # For most gems, this is effectively the same as `enforce_insync: true`
43
- # major: 0
44
- # minor: 6
45
- # patch: 0
46
- server:
47
- source: https://rubygems.org
38
+ behaviors:
39
+ - on_failure:
40
+ log_level: error
41
+ behavior: exit # on failure, log error and exit(1)
42
+ version_enforce:
43
+ insync: true # Ensure gem is up to date
44
+ server: https://rubygems.org
48
45
 
49
46
  custom_gem:
50
- enforce_insync: true
47
+ behaviors:
48
+ - version_enforce:
49
+ insync: true
51
50
  server: true
51
+
52
52
  sidekiq:
53
- version_threshold:
54
- releases: 5 # within the last 5 releases
55
- git:
56
- owner: sidekiq # Github root page for the gem
53
+ behaviors:
54
+ - version_enforce:
55
+ insync: truee
56
+ git: sidekiq # Github root page for the gem
57
57
 
58
58
 
59
59
 
@@ -2,5 +2,6 @@
2
2
 
3
3
  module GemEnforcer
4
4
  class Error < StandardError; end
5
+ class ConfigError < Error; end
5
6
  class ValidationError < Error; end
6
7
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ require "gem_enforcer/setup/helper/on_failure"
5
+ require "gem_enforcer/setup/helper/version_enforcer"
6
+
7
+ module GemEnforcer
8
+ module Setup
9
+ class Behavior
10
+ attr_reader :gem_name, :params, :index
11
+
12
+ def initialize(gem_name:, index:, **params)
13
+ @gem_name = gem_name
14
+ @index = index
15
+ @params = params.transform_keys(&:to_sym)
16
+ end
17
+
18
+ def valid_config?
19
+ @valid_config ||= validate_config
20
+ end
21
+
22
+ def run_behavior!(version_list:, version:)
23
+ unless valid_config?
24
+ raise ConfigError, "Attempted to run validations with invalid Version Configurations"
25
+ end
26
+
27
+ return true if version.nil?
28
+ return true if version_enforcer.valid_gem_versions?(version_list:, version:)
29
+
30
+ false
31
+ end
32
+
33
+ def error_status
34
+ return nil if errors.empty?
35
+
36
+ errors.map { "behaviors[#{index}].#{_1}" }
37
+ end
38
+
39
+ def version_enforcer
40
+ @version_enforcer ||= Helper::VersionEnforcer.new(gem_name:, version_enforce: params[:version_enforce])
41
+ end
42
+
43
+ def on_failure
44
+ @on_failure ||= Helper::OnFailure.new(gem_name:, on_failure: params[:on_failure])
45
+ end
46
+
47
+ def run_failure!(message:, version:, version_list:)
48
+ params = {
49
+ version:,
50
+ c: version_list.max,
51
+ versions_behind: version_enforcer.versions_behind(version_list:, version:),
52
+ }
53
+ on_failure.run_on_failure!(message:, **params)
54
+ end
55
+
56
+ private
57
+
58
+ def errors
59
+ @errors ||= []
60
+ end
61
+
62
+ def validate_config
63
+ boolean = version_enforcer.valid_config?
64
+ boolean &= on_failure.valid_config?
65
+
66
+ @errors = Array(version_enforcer.errors).compact + Array(on_failure.errors).compact
67
+ @errors.length == 0
68
+ end
69
+ end
70
+ end
71
+ end
@@ -3,54 +3,78 @@
3
3
  module GemEnforcer
4
4
  module Setup
5
5
  module Helper
6
- module OnFailure
7
- ALLOWED_ON_FAILURE = [:raise, :exit, DEFAULT_BEHAVIOR = :none]
6
+ class OnFailure
7
+ attr_reader :gem_name, :on_failure
8
+
9
+ ALLOWED_KEYS = [LOG_LEVEL = :log_level, FAILURE_BEHAVIOR = :behavior, MESSAGE = :message]
10
+ ALLOWED_FAILURE_BEHAVIOR = [:raise, :exit, DEFAULT_BEHAVIOR = :none]
8
11
  DEFAULT_LOG_LEVEL = :error
9
12
 
10
- def validate_on_failure
11
- on_failure = params["on_failure"]
12
- if on_failure.nil?
13
- @on_failure_log_level = DEFAULT_LOG_LEVEL
14
- @on_failure_behavior = DEFAULT_BEHAVIOR
15
- return true
13
+ def initialize(gem_name:, on_failure:)
14
+ @gem_name = gem_name
15
+ @on_failure = on_failure.transform_keys(&:to_sym) rescue on_failure
16
+ end
17
+
18
+ def valid_config?
19
+ @valid_config ||= validate_config
20
+ end
21
+
22
+ def errors
23
+ @errors ||= []
24
+ end
25
+
26
+ def run_on_failure!(message:, **params)
27
+ unless valid_config?
28
+ raise ConfigError, "Attempted to run on_failure with an invalid config."
16
29
  end
17
30
 
18
- if Hash === on_failure
19
- @on_failure_log_level = on_failure.fetch("log_level", DEFAULT_LOG_LEVEL)
20
- behavior = on_failure.fetch("behavior", DEFAULT_BEHAVIOR).to_sym rescue nil
21
- if ALLOWED_ON_FAILURE.include?(behavior)
22
- @on_failure_behavior = behavior
23
- return true
24
- else
25
- errors << "on_failure.behavior: Expected behavior to be in #{ALLOWED_ON_FAILURE}"
26
- return false
27
- end
31
+ send_message = (provided_message || message) % params
32
+ GemEnforcer.logger.public_send(on_failure_log_level, send_message)
33
+
34
+ case on_failure_behavior.to_sym
35
+ when :raise
36
+ raise ValidationError, send_message
37
+ when :exit
38
+ Kernel.exit(1)
28
39
  end
29
40
 
30
- errors << "on_failure: Expected value hash with :behavior and/or :log_level keys"
31
- false
41
+ true
32
42
  end
33
43
 
34
- def on_failure_default_message
35
- message = params.dig("on_failure", "message") rescue nil
36
- return message if message
44
+ private
37
45
 
38
- version_default_message
46
+ def on_failure_behavior
47
+ (on_failure[FAILURE_BEHAVIOR] || DEFAULT_BEHAVIOR) rescue DEFAULT_BEHAVIOR
39
48
  end
40
49
 
41
- def on_failure_behavior(msg:)
50
+ def on_failure_log_level
51
+ (on_failure[LOG_LEVEL] || DEFAULT_LOG_LEVEL) rescue DEFAULT_LOG_LEVEL
52
+ end
42
53
 
54
+ def provided_message
55
+ on_failure[MESSAGE] rescue nil
43
56
  end
44
57
 
45
- def execute_on_failure!(behavior:, msg: on_failure_default_message)
46
- GemEnforcer.logger.public_send(@on_failure_log_level, "Validation failed for #{gem_name}. Current Version is #{current_version}. #{msg}")
58
+ def validate_config
59
+ return true if on_failure.nil?
47
60
 
48
- case @on_failure_behavior.to_sym
49
- when :raise
50
- raise ValidationError, "Validation failed for #{gem_name}. Current Version is #{current_version}. #{msg}"
51
- when :exit
52
- Kernel.exit(1)
61
+ unless Hash === on_failure
62
+ errors << "on_failure: Expected to contain a Hash. Contained a [#{on_failure.class}]"
63
+ return false
53
64
  end
65
+
66
+ disallowed_keys = on_failure.keys - ALLOWED_KEYS
67
+ if disallowed_keys.length > 0
68
+ errors << "on_failure: Contained unexpected keys. Only #{ALLOWED_KEYS} are allowed. Found #{disallowed_keys}"
69
+ return false
70
+ end
71
+
72
+ if on_failure[FAILURE_BEHAVIOR] && ALLOWED_FAILURE_BEHAVIOR.none? { _1 == on_failure[FAILURE_BEHAVIOR].to_sym }
73
+ errors << "on_failure.#{FAILURE_BEHAVIOR}: Value must be one of #{ALLOWED_FAILURE_BEHAVIOR}. Provided [#{on_failure[FAILURE_BEHAVIOR]}] "
74
+ return false
75
+ end
76
+
77
+ true
54
78
  end
55
79
  end
56
80
  end
@@ -3,56 +3,65 @@
3
3
  module GemEnforcer
4
4
  module Setup
5
5
  module Helper
6
- module Retrieval
7
- def validate_retrieval
8
- if params["server"]
9
- @retrieval_method = :server
10
- server_result = _server_validate
11
- @factory_version_list = Retrieve.server_retrieval_by_source(source: @retrieval_source)
12
-
13
- server_result
14
- elsif params["git"]
15
- @retrieval_method = :git
16
- git_result = _git_validate
17
- @factory_version_list = Retrieve.github_retrieval_by_owner(owner: @retrieval_owner)
18
-
19
- git_result
20
- else
21
- errors << "retrieval: Missing retrieval type. Expected `server` or `git`"
22
- false
23
- end
6
+ class Retrieval
7
+ attr_reader :gem_name, :server, :git, :retrieval_factory
8
+
9
+ def initialize(gem_name:, server:, git:)
10
+ @gem_name = gem_name
11
+ @server = server
12
+ @git = git
13
+ end
14
+
15
+ def valid_config?
16
+ @valid_config ||= validate_config
17
+ end
18
+
19
+ def errors
20
+ @errors ||= []
24
21
  end
25
22
 
26
23
  def retrieve_version_list
27
- @factory_version_list.gem_versions(name: @gem_name).sort
24
+ unless valid_config?
25
+ raise ConfigError, "Attempted to run validations with invalid Version Configurations"
26
+ end
27
+
28
+ retrieval_factory.gem_versions(name: gem_name).sort
28
29
  end
29
30
 
30
- def _server_validate
31
- server = params.dig("server")
31
+ private
32
32
 
33
- if server == true
34
- @retrieval_source = GemEnforcer::DEFAULT_SERVER_SOURCE
35
- return true
33
+ def validate_config
34
+ if server && git
35
+ errors << "retrieval: `server` and `git` keys present. Must only choose 1"
36
+ return false
36
37
  end
37
38
 
38
- if Hash === server && server["source"]
39
- @retrieval_source = server["source"]
39
+ if server
40
+ if server == true
41
+ @source = GemEnforcer::DEFAULT_SERVER_SOURCE
42
+ elsif String === server
43
+ @source = server
44
+ else
45
+ errors << "server: Server retrieval provided. Expected `true` or a string of the rubygem source endpoint"
46
+ return false
47
+ end
48
+ @retrieval_factory = Retrieve.server_retrieval_by_source(source: @source)
40
49
  return true
41
50
  end
42
51
 
43
- errors << "retrieval.server: Missing source"
44
- false
45
- end
46
-
47
- def _git_validate
48
- git = params.dig("git")
49
- if Hash === git && git["owner"]
50
- @retrieval_owner = git["owner"]
51
- return true
52
+ if git
53
+ if String === git
54
+ @owner = git
55
+ @retrieval_factory = Retrieve.github_retrieval_by_owner(owner: git)
56
+ return true
57
+ else
58
+ errors << "git: Git retrieval provided. Expected string of the owner/organization of the gem"
59
+ return false
60
+ end
52
61
  end
53
62
 
54
- errors << "retrieval.git: Missing owner"
55
- false
63
+ errors << "retrieval: `server` and `git` keys are missing. Must provide 1 retrieval method"
64
+ return false
56
65
  end
57
66
  end
58
67
  end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemEnforcer
4
+ module Setup
5
+ module Helper
6
+ class VersionEnforcer
7
+ attr_reader :gem_name, :version_enforce, :error_validation_message
8
+
9
+ ALLOWED_VERSION_INSYNC = :insync
10
+ ALLOWED_VERSION_REALEASE = :releases
11
+ ALLOWED_VERSION_SEMVER = [MAJOR = :major, MINOR = :minor, PATCH = :patch]
12
+
13
+ def initialize(gem_name:, version_enforce:)
14
+ @gem_name = gem_name
15
+ @version_enforce = version_enforce.transform_keys(&:to_sym) rescue nil
16
+ end
17
+
18
+ def valid_config?
19
+ @valid_config ||= validate_config
20
+ end
21
+
22
+ def valid_gem_versions?(version_list:, version:)
23
+ unless valid_config?
24
+ raise ConfigError, "Attempted to run validations with invalid Version Configurations"
25
+ end
26
+
27
+ return true if version.nil?
28
+
29
+ min_to_max_version_sorted_list = version_list.sort
30
+ case @version_type
31
+ when :insync
32
+ __validation_insync(version_list: min_to_max_version_sorted_list, version:)
33
+ when :releases
34
+ __validation_releases(version_list: min_to_max_version_sorted_list, version:)
35
+ when :semver
36
+ __validation_semver(version_list: min_to_max_version_sorted_list, version:)
37
+ end
38
+ end
39
+
40
+ def errors
41
+ @errors ||= []
42
+ end
43
+
44
+ def versions_behind(version_list:, version:)
45
+ version_list.sort.reverse.find_index(version)
46
+ end
47
+
48
+ private
49
+
50
+ def __validation_insync(version_list:, version:)
51
+ max_version = version_list.max
52
+ return true if version >= max_version
53
+
54
+ @error_validation_message = "[#{gem_name}] Enforcer expects the most recent version. Version #{version}. Most Recent version #{max_version}"
55
+
56
+ false
57
+ end
58
+
59
+ def __validation_releases(version_list:, version:)
60
+ releases_behind = version_enforce[ALLOWED_VERSION_REALEASE]
61
+ min_version_allowed = version_list[-releases_behind]
62
+ return true if version >= min_version_allowed
63
+
64
+ if index = version_list.sort.reverse.find_index(version)
65
+ version_text = "Version [#{version}] is the #{index} oldest version."
66
+ else
67
+ version_text = "Version [#{version}] is the was not found in the provided list."
68
+ end
69
+
70
+ @error_validation_message = "[#{gem_name}] Enforcer expects the version to be within the most recent #{releases_behind} versions. #{version_text}"
71
+ false
72
+ end
73
+
74
+ def __validation_semver(version_list:, version:)
75
+ if max_major_versions_behind = version_enforce[:major]
76
+ if error = __threshold_semver_distance(type: :major, number: version.segments[0], list: version_list.map { _1.segments[0] }, threshold: max_major_versions_behind, version: version)
77
+ @error_validation_message = error
78
+ return false
79
+ end
80
+ end
81
+
82
+ if max_minor_versions_behind = version_enforce[:minor]
83
+ # Select only the minor versions that match the major version
84
+ current_major_version = version.segments[0]
85
+ minor_version_check_list = version_list.select { _1.segments[0] == current_major_version }.map { _1.segments[1] }
86
+ if error = __threshold_semver_distance(type: :minor, number: version.segments[1], list: minor_version_check_list, threshold: max_minor_versions_behind, version: version)
87
+ @error_validation_message = error
88
+ return false
89
+ end
90
+ end
91
+
92
+ if max_patch_versions_behind = version_enforce[:patch]
93
+ # Select only the patch versions that match the major.minor version
94
+ current_major_minor_version = version.segments[0..1]
95
+ patch_version_check_list = version_list.select { _1.segments[0..1] == current_major_minor_version }.map { _1.segments[2] }
96
+ if error = __threshold_semver_distance(type: :patch, number: version.segments[2], list: patch_version_check_list, threshold: max_patch_versions_behind, version: version)
97
+ @error_validation_message = error
98
+ return false
99
+ end
100
+ end
101
+
102
+ true
103
+ end
104
+
105
+ def __threshold_semver_distance(type:, number:, list:, threshold:, version:)
106
+ # remove duplicates ans sort in highest to lowest number
107
+ uniq_list = list.uniq.sort.reverse
108
+
109
+ # get the position in the sorted array
110
+ position_in_sorted_array = uniq_list.find_index(number)
111
+
112
+ # if position is less than or equal to the threshold, we are good
113
+ # otherwise, it is out of compliance
114
+ return nil if position_in_sorted_array <= threshold
115
+
116
+ "[#{gem_name}] Enforcer expects the version to be within #{threshold} #{type} versions of the most recent version. Version is #{version}"
117
+ end
118
+
119
+ def validate_config
120
+ unless Hash === version_enforce
121
+ errors << "version_enforce: Must be a hash containing [#{ALLOWED_VERSION_INSYNC}] or [#{ALLOWED_VERSION_REALEASE}] or any of [#{ALLOWED_VERSION_SEMVER}]"
122
+ return false
123
+ end
124
+
125
+ if version_enforce.keys.include?(ALLOWED_VERSION_INSYNC)
126
+ @version_type = :insync
127
+ __validate_config_insync
128
+ elsif version_enforce.keys.include?(ALLOWED_VERSION_REALEASE)
129
+ @version_type = :releases
130
+ __validate_config_releases
131
+ elsif version_enforce.keys.any? { ALLOWED_VERSION_SEMVER.include?(_1) }
132
+ @version_type = :semver
133
+ __validate_config_semver
134
+ else
135
+ errors << "version_enforce: Invalid config. Hash must contain [#{ALLOWED_VERSION_INSYNC}] or [#{ALLOWED_VERSION_REALEASE}] or any of [#{ALLOWED_VERSION_SEMVER}]"
136
+ false
137
+ end
138
+ end
139
+
140
+ def __validate_config_insync
141
+ return false unless validate_expected_keys(ALLOWED_VERSION_INSYNC, "insync")
142
+
143
+ return true if version_enforce[ALLOWED_VERSION_INSYNC]
144
+
145
+ errors << "version_enforce.#{ALLOWED_VERSION_INSYNC}: When key is present, value must be true. Received [#{version_enforce[ALLOWED_VERSION_INSYNC]}]"
146
+ false
147
+ end
148
+
149
+ def __validate_config_releases
150
+ return false unless validate_expected_keys(ALLOWED_VERSION_REALEASE, ALLOWED_VERSION_REALEASE)
151
+
152
+ validate_integer(version_enforce[ALLOWED_VERSION_REALEASE], ALLOWED_VERSION_REALEASE)
153
+ end
154
+
155
+ def __validate_config_semver
156
+ return false unless validate_expected_keys(ALLOWED_VERSION_SEMVER, "SemVer")
157
+
158
+ boolean = true
159
+ boolean &= validate_integer(version_enforce[MAJOR], "major") if version_enforce[MAJOR]
160
+ boolean &= validate_integer(version_enforce[MINOR], "minor") if version_enforce[MINOR]
161
+ boolean &= validate_integer(version_enforce[PATCH], "patch") if version_enforce[PATCH]
162
+
163
+ boolean
164
+ end
165
+
166
+ def validate_expected_keys(allowed, type)
167
+ disallowed_keys = version_enforce.keys - Array(allowed)
168
+
169
+ return true if disallowed_keys.length == 0
170
+
171
+ errors << "version_enforce: Unexpected keys present for `#{type}`. Allowed keys [#{allowed}] but received #{version_enforce.keys}. [#{disallowed_keys}] is not allowed"
172
+ false
173
+ end
174
+
175
+ def validate_integer(val, dig)
176
+ return true if Integer === val
177
+
178
+ errors << "version_enforce.#{dig}: Expected value to be an Integer. Recieved type #{val.class} [#{val}]"
179
+ false
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,38 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
3
+ require "gem_enforcer/setup/behavior"
5
4
  require "gem_enforcer/setup/helper/retrieval"
6
- require "gem_enforcer/setup/helper/on_failure"
7
- require "gem_enforcer/setup/helper/version"
8
5
 
9
6
  module GemEnforcer
10
7
  module Setup
11
8
  class Validate
12
- attr_reader :gem_name, :params, :validation_status
13
-
14
- include Helper::Retrieval
15
- include Helper::OnFailure
16
- include Helper::Version
9
+ attr_reader :gem_name, :params, :behaviors, :retrieval
17
10
 
18
11
  def initialize(name:, **params)
19
12
  @params = params
20
13
  @gem_name = name
21
-
22
- @validation_status = validate!
14
+ @behaviors = []
15
+ @errors = []
23
16
  end
24
17
 
25
- # Allow behavior to be overridden if desired
26
- def run_validation!(behavior: nil)
27
- unless validation_status
18
+ def run_validation!
19
+ unless valid_config?
28
20
  raise Error, "Unable to run validation with invalid config."
29
21
  end
30
-
31
22
  return true if current_version.nil?
32
23
 
33
- return true if version_execute?(version_list: retrieve_version_list)
24
+ version_list = retrieval.retrieve_version_list
25
+ passed_behavior, failed_behavior = behaviors.partition { _1.run_behavior!(version_list:, version: current_version) }
26
+
27
+ return true if failed_behavior.empty?
28
+
29
+
30
+ failed_behavior.each do |failed|
31
+ default_message = failed.version_enforcer.error_validation_message
32
+ failed.run_failure!(message: default_message, version: current_version, version_list: version_list)
33
+ end
34
34
 
35
- execute_on_failure!(behavior: behavior)
36
35
  false
37
36
  end
38
37
 
@@ -40,6 +39,10 @@ module GemEnforcer
40
39
  Gem.loaded_specs[gem_name]&.version
41
40
  end
42
41
 
42
+ def valid_config?
43
+ @valid_config ||= validate_config
44
+ end
45
+
43
46
  def error_status
44
47
  return nil if errors.empty?
45
48
 
@@ -48,17 +51,47 @@ module GemEnforcer
48
51
 
49
52
  private
50
53
 
51
- def errors
52
- @errors ||= []
53
- end
54
+ def validate_config
55
+ generate_behaviors!
56
+ generate_retreival!
57
+
58
+ boolean = true
59
+ if behaviors.length == 0
60
+ @errors << "behaviors: At least 1 behavior is expected per gem validation"
61
+ boolean = false
62
+ else
63
+ unless behaviors.all? { _1.valid_config? }
64
+ # at least 1 beavior failed validation
65
+ behaviors.each do |b|
66
+ @errors += b.error_status if b.error_status
67
+ end
68
+ errors.flatten!
69
+ boolean = false
70
+ end
71
+ end
54
72
 
55
- def validate!
56
- boolean = validate_retrieval
57
- boolean &= validate_on_failure
58
- boolean &= validate_version
73
+ unless retrieval.valid_config?
74
+ @errors += retrieval.errors
75
+ boolean = false
76
+ end
59
77
 
60
78
  boolean
61
79
  end
80
+
81
+ def generate_behaviors!
82
+ raw_behaviors = Array(params["behaviors"] || params[:behaviors] || [])
83
+ raw_behaviors.each_with_index do |behavior, index|
84
+ @behaviors << Behavior.new(index:, gem_name:, **behavior)
85
+ end
86
+ end
87
+
88
+ def generate_retreival!
89
+ @retrieval = Helper::Retrieval.new(gem_name:, server: (params[:server] || params["server"]), git: (params[:git] || params["git"]))
90
+ end
91
+
92
+ def errors
93
+ @errors ||= []
94
+ end
62
95
  end
63
96
  end
64
97
  end
@@ -8,24 +8,37 @@ module GemEnforcer
8
8
  module_function
9
9
 
10
10
  def validate_yml!
11
- errors = config_yml["gems"].map do |name, metadata|
11
+ @errors = config_yml["gems"].map do |name, metadata|
12
12
  validator = Validate.new(name: name, **metadata)
13
13
  validations << validator
14
14
 
15
- validator.error_status
16
- end.compact
15
+ validator.error_status unless validator.valid_config?
16
+ end.flatten.compact
17
17
  return true if errors.empty?
18
18
 
19
19
  log_level = config_yml.dig("invalid_config", "log_level") || "error"
20
20
  behavior = config_yml.dig("invalid_config", "behavior") || "exit"
21
+
22
+ false
23
+ end
24
+
25
+ def errors
26
+ @errors ||= []
21
27
  end
22
28
 
23
29
  def validations
24
30
  @validations ||= []
25
31
  end
26
32
 
27
- def run_validations!(behavior: nil)
28
- validations.each { _1.run_validation!(behavior: behavior) }
33
+ def run_validations!
34
+ validations.each { _1.run_validation! }
35
+ end
36
+
37
+ def execute!
38
+ if validate_yml!
39
+
40
+ else
41
+ end
29
42
  end
30
43
 
31
44
  def config_yml
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemEnforcer
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_enforcer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Taylor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-11 00:00:00.000000000 Z
11
+ date: 2024-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: class_composer
@@ -94,20 +94,6 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.17.0
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: 0.17.0
111
97
  description: Provide the ability to validate targetted gems are up to date before
112
98
  executing commands
113
99
  email:
@@ -129,6 +115,10 @@ files:
129
115
  - README.md
130
116
  - bin/console
131
117
  - docker-compose.yml
118
+ - examples/README.md
119
+ - examples/git_tag.yml
120
+ - examples/multiple_behavior.yml
121
+ - examples/single_validation.yml
132
122
  - gem_enforcer.gemspec
133
123
  - gem_enforcer.yml
134
124
  - lib/gem_enforcer.rb
@@ -138,9 +128,10 @@ files:
138
128
  - lib/gem_enforcer/retrieve/gem_server.rb
139
129
  - lib/gem_enforcer/retrieve/git_tag.rb
140
130
  - lib/gem_enforcer/setup.rb
131
+ - lib/gem_enforcer/setup/behavior.rb
141
132
  - lib/gem_enforcer/setup/helper/on_failure.rb
142
133
  - lib/gem_enforcer/setup/helper/retrieval.rb
143
- - lib/gem_enforcer/setup/helper/version.rb
134
+ - lib/gem_enforcer/setup/helper/version_enforcer.rb
144
135
  - lib/gem_enforcer/setup/validate.rb
145
136
  - lib/gem_enforcer/version.rb
146
137
  homepage: https://github.com/matt-taylor/gem_enforcer
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemEnforcer
4
- module Setup
5
- module Helper
6
- module Version
7
- ALLOWED_VERSION_REALEASE = :releases
8
- ALLOWED_VERSION_SEMVER = [:major, :minor, :patch]
9
- ALLOWED_VERSION_THRESHOLD_KEYS = [ALLOWED_VERSION_REALEASE, *ALLOWED_VERSION_SEMVER]
10
-
11
- def version_default_message
12
- @default_version_message
13
- end
14
-
15
- def validate_version
16
- if params.keys.include?("enforce_insync") && params.keys.include?("version_threshold")
17
- errors << "version: Must only contain `enforce_insync` or `version_threshold`"
18
- return false
19
- end
20
-
21
- if params["enforce_insync"]
22
- @version_type = :enforce_insync
23
- @default_version_message = "Version must be the most recently released version."
24
- return true
25
- end
26
-
27
- @version_type = :version_threshold
28
- version_threshold_keys = params["version_threshold"]&.keys || []
29
- if version_threshold_keys.sort == [ALLOWED_VERSION_REALEASE.to_s].sort
30
- @version_threshold = :releases
31
- @default_version_message = "Version must be within #{params["version_threshold"]["releases"]} of the most recently released versions"
32
- _releases_validate
33
- elsif ALLOWED_VERSION_THRESHOLD_KEYS.any? { version_threshold_keys.include?(_1.to_s) }
34
- @version_threshold = :semver
35
- message = []
36
- message << "#{params["version_threshold"]["major"]} major versions" if params["version_threshold"]["major"]
37
- message << "#{params["version_threshold"]["minor"]} minor versions" if params["version_threshold"]["minor"]
38
- message << "#{params["version_threshold"]["patch"]} patch versions" if params["version_threshold"]["patch"]
39
- @default_version_message = "Version must be within #{message.join(" and ")} of the most recent release."
40
- _semver_validate
41
- else
42
- errors << "version.version_threshold: Expected keys to contain [#{ALLOWED_VERSION_REALEASE}] or #{ALLOWED_VERSION_SEMVER}"
43
- false
44
- end
45
- end
46
-
47
- def _semver_validate
48
- boolean = true
49
- if major = params["version_threshold"]["major"]
50
- unless Integer === major
51
- boolean = false
52
- errors << "version.version_threshold.major: Expected value to be an Integer"
53
- end
54
- end
55
-
56
- if minor = params["version_threshold"]["minor"]
57
- unless Integer === minor
58
- boolean = false
59
- errors << "version.version_threshold.minor: Expected value to be an Integer"
60
- end
61
- end
62
-
63
- if patch = params["version_threshold"]["patch"]
64
- unless Integer === patch
65
- boolean = false
66
- errors << "version.version_threshold.patch: Expected value to be an Integer"
67
- end
68
- end
69
-
70
- boolean
71
- end
72
-
73
- def _releases_validate
74
- release_count = params["version_threshold"]["releases"]
75
- unless Integer === release_count
76
- errors << "version.version_threshold.releases: Expected value to be an Integer"
77
- return false
78
- end
79
-
80
- true
81
- end
82
-
83
- def version_execute?(version_list:)
84
- if @version_type == :enforce_insync
85
- __validate_enforce_insync?(version_list:)
86
- else
87
- if @version_threshold == :releases
88
- __validate_version_threshold_releases?(version_list:)
89
- else
90
- __validate_version_threshold_semver?(version_list:)
91
- end
92
- end
93
- end
94
-
95
- def __validate_version_threshold_semver?(version_list:)
96
- if max_major_versions_behind = params.dig("version_threshold", "major")
97
- return false unless __threshold_semver_distance(type: :major, number: current_version.segments[0], list: version_list.map { _1.segments[0] }, threshold: max_major_versions_behind)
98
- end
99
-
100
- if max_minor_versions_behind = params.dig("version_threshold", "minor")
101
- # Select only the minor versions that match the major version
102
- current_major_version = current_version.segments[0]
103
- minor_version_check_list = version_list.select { _1.segments[0] == current_major_version }.map { _1.segments[1] }
104
- return false unless __threshold_semver_distance(type: :minor, number: current_version.segments[1], list: minor_version_check_list, threshold: max_minor_versions_behind)
105
- end
106
-
107
- if max_patch_versions_behind = params.dig("version_threshold", "patch")
108
- # Select only the patch versions that match the major version
109
- current_major_minor_version = current_version.segments[0..1]
110
- patch_version_check_list = version_list.select { _1.segments[0..1] == current_major_minor_version }.map { _1.segments[2] }
111
- return false unless __threshold_semver_distance(type: :patch, number: current_version.segments[2], list: patch_version_check_list, threshold: max_patch_versions_behind)
112
- end
113
-
114
- true
115
- end
116
-
117
- def __threshold_semver_distance(type:, number:, list:, threshold:)
118
- # remove duplicates ans sort in highest to lowest number
119
- uniq_list = list.uniq.sort.reverse
120
-
121
- # get the position in the sorted array
122
- position_in_sorted_array = uniq_list.find_index(number)
123
-
124
- # if position is less than or equal to the threshold, we are good
125
- # otherwise, it is out of compliance
126
- return true if position_in_sorted_array <= threshold
127
-
128
- @default_version_message += " Failed to match #{type} version threshold"
129
-
130
- false
131
- end
132
-
133
- def __validate_version_threshold_releases?(version_list:)
134
- releases_behind = params.dig("version_threshold", "releases").to_i
135
-
136
- min_version_allowed = version_list[-releases_behind]
137
- current_version >= min_version_allowed
138
- end
139
-
140
- def __validate_enforce_insync?(version_list:)
141
- max_version = version_list.max
142
- return true if current_version >= max_version
143
-
144
- @default_version_message += " Please upgrade to at least v#{max_version}"
145
-
146
- false
147
- end
148
- end
149
- end
150
- end
151
- end