prependers 0.3.0 → 1.0.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: c33986e6c228a6eea44d3093e16717cb5327b57f23439bc492a8bea1154b7be8
4
- data.tar.gz: f4e7cc9175ba8b51a242346f8ee5d08dcee79fe4befb0f806a45706f2b61f394
3
+ metadata.gz: 30c53a5f64db0e3ee8454289403fbf6aa00a7d90fdee76c504a5cc878324ef88
4
+ data.tar.gz: 14db78d2c2160984cd891f35f9b1794e55ec011068db5c50834538df430eebf2
5
5
  SHA512:
6
- metadata.gz: b71e78f24c52085bac6e5102306626097601799bb377c8e9dbb6bf768122fccd3a4cc853164c13f98239ba9db722426319c766f261fe1cc303fcc8220c2d0f37
7
- data.tar.gz: a105daf60dd5b44393f9719ad1b2c1913a52cb2d6a90f5d9d2486a4ee6a1672b7c07d38693487f22568739e53bfb851d3858807b66bdc33a24fffcaeb8882922
6
+ metadata.gz: '0660258db42ece336874d2868b93652a89128076a612057144861a8961d61c1330d8d65d0b4369a81ad2361fdd3d33eeb634b4dab72921a5a1a027a308ba5ecc'
7
+ data.tar.gz: ab60f96d45f3fecb4abb1689260e6f5aa8b47d52700035ab6a33aebd0e111bf56fb1053d33b380474224a56dab9b1fa11e32bb5e047393a5d2f4e6eacfc21d12
@@ -1,5 +1,5 @@
1
1
  # Relaxed.Ruby.Style
2
- ## Version 2.4
2
+ ## Version 2.5
3
3
 
4
4
  Style/Alias:
5
5
  Enabled: false
@@ -145,30 +145,9 @@ Lint/AssignmentInCondition:
145
145
  Enabled: false
146
146
  StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
147
147
 
148
- Metrics/AbcSize:
148
+ Layout/LineLength:
149
149
  Enabled: false
150
150
 
151
- Metrics/BlockNesting:
152
- Enabled: false
153
-
154
- Metrics/ClassLength:
155
- Enabled: false
156
-
157
- Metrics/ModuleLength:
158
- Enabled: false
159
-
160
- Metrics/CyclomaticComplexity:
161
- Enabled: false
162
-
163
- Metrics/LineLength:
164
- Enabled: false
165
-
166
- Metrics/MethodLength:
167
- Enabled: false
168
-
169
- Metrics/ParameterLists:
170
- Enabled: false
171
-
172
- Metrics/PerceivedComplexity:
151
+ Metrics:
173
152
  Enabled: false
174
153
 
@@ -12,5 +12,11 @@ AllCops:
12
12
  Metrics/BlockLength:
13
13
  Enabled: false
14
14
 
15
+ RSpec/ExampleLength:
16
+ Enabled: false
17
+
15
18
  Style/GuardClause:
16
19
  Enabled: false
20
+
21
+ Style/FormatStringToken:
22
+ EnforcedStyle: template
@@ -1,8 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/nebulab/prependers/tree/HEAD)
3
+ ## [v0.3.0](https://github.com/nebulab/prependers/tree/v0.3.0) (2020-04-24)
4
4
 
5
- [Full Changelog](https://github.com/nebulab/prependers/compare/v0.2.0...HEAD)
5
+ [Full Changelog](https://github.com/nebulab/prependers/compare/v0.2.0...v0.3.0)
6
6
 
7
7
  **Merged pull requests:**
8
8
 
data/README.md CHANGED
@@ -18,7 +18,6 @@ And then execute:
18
18
  $ bundle
19
19
  ```
20
20
 
21
-
22
21
  Or install it yourself as:
23
22
 
24
23
  ```console
@@ -27,13 +26,13 @@ $ gem install prependers
27
26
 
28
27
  ## Usage
29
28
 
30
- To define a prepender manually, simply include the `Prependers::Prepender.new` module. For instance,
29
+ To define a prepender manually, simply include the `Prependers::Prepender[]` module. For instance,
31
30
  if you have installed an `animals` gem and you want to extend the `Animals::Dog` class, you can
32
31
  define a module like the following:
33
32
 
34
33
  ```ruby
35
34
  module Animals::Dog::AddBarking
36
- include Prependers::Prepender.new
35
+ include Prependers::Prepender[]
37
36
 
38
37
  def bark
39
38
  puts 'Woof!'
@@ -50,7 +49,7 @@ prepender:
50
49
 
51
50
  ```ruby
52
51
  module Animals::Dog::AddFamily
53
- include Prependers::Prepender.new
52
+ include Prependers::Prepender[]
54
53
 
55
54
  module ClassMethods
56
55
  def family
@@ -62,13 +61,100 @@ end
62
61
  Animals::Dog.family # => 'Canids'
63
62
  ```
64
63
 
65
- As you can see, the `ClassMethods` module has automagically been `prepend`ed to the `Animals::Dog`'s
64
+ As you can see, the `ClassMethods` module has automagically been `prepend`ed to `Animals::Dog`'s
66
65
  singleton class.
67
66
 
67
+ ### Using a namespace
68
+
69
+ It can be useful to have a prefix namespace for your prependers. That way, you don't have to worry
70
+ about accidentally overriding any vendor modules. This is actually the recommended way to define
71
+ your prependers.
72
+
73
+ You can accomplish this by passing the `:namespace` option when including `Prependers::Prepender`:
74
+
75
+ ```ruby
76
+ module MyApp
77
+ module Animals
78
+ module Dog
79
+ module AddBarking
80
+ include Prependers::Prepender[namespace: MyApp]
81
+
82
+ def bark
83
+ puts 'Woof!'
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ ### Verifying original sources
92
+
93
+ One issue you may run into when extending third-party code is that, when the original implementation
94
+ is updated, it's not always obvious whether you have to update any of your extensions.
95
+
96
+ Prependers make this a bit easier with the concept of original source verification: you can compute
97
+ a SHA1 hash of the original implementation, store it along with your prepender, and then verify it
98
+ against the current hash when your application loads. If the original source changes, you get an
99
+ error asking you to ensure your prepender is still relevant.
100
+
101
+ To use original source verification in your prependers, pass the `:verify` option:
102
+
103
+ ```ruby
104
+ module Animals::Dog::AddBarking
105
+ include Prependers::Prepender[verify: nil]
106
+
107
+ # ...
108
+ end
109
+ ```
110
+
111
+ When you load your application now, you will get an error with instructions on how to set the proper
112
+ hash:
113
+
114
+ ```
115
+ Prependers::OutdatedPrependerError:
116
+ You have not defined an original hash for Animals::Dog in Animals::Dog::AddBarking.
117
+
118
+ You can define the hash by updating your include statement as follows:
119
+
120
+ include Prependers::Prepender[verify: 'f7175533215c39f3f3328aa5829ac6b1bb168218']
121
+ ```
122
+
123
+ At this point, you should update your prepender with the correct hash:
124
+
125
+ ```ruby
126
+ module Animals::Dog::AddBarking
127
+ include Prependers::Prepender[verify: 'f7175533215c39f3f3328aa5829ac6b1bb168218']
128
+
129
+ # ...
130
+ end
131
+ ```
132
+
133
+ Now, when the underlying implementation of `Animals::Dog` changes because of a dependency update or
134
+ other reasons, Prependers will raise an error such as the following:
135
+
136
+ ```
137
+ Prependers::OutdatedPrependerError:
138
+ The stored hash for Animals::Dog in Animals::Dog::AddBarking is
139
+ f7175533215c39f3f3328aa5829ac6b1bb168218, but the current hash is
140
+ 2f05682e4f46b509c23a8418d9427a9eeaa8a79e instead.
141
+
142
+ This most likely means that the original source has changed.
143
+
144
+ Check that your prepender is still valid, then update the stored hash:
145
+
146
+ include Prependers::Prepender[verify: '2f05682e4f46b509c23a8418d9427a9eeaa8a79e']
147
+ ```
148
+
149
+ Original source verification also works when a module is defined in multiple locations.
150
+
151
+ *NOTE: Due to limitations in Ruby's API, it is not possible to use source verification with modules
152
+ that don't define any methods. Prependers will raise an error if you try to do this.*
153
+
68
154
  ### Autoloading prependers
69
155
 
70
- If you don't want to include `Prependers::Prepender`, you can also autoload prependers from a path,
71
- they will be loaded in alphabetical order.
156
+ If you don't want to include `Prependers::Prepender[]`, you can also autoload prependers from a
157
+ path, they will be loaded in alphabetical order.
72
158
 
73
159
  Here's the previous example, but with autoloading:
74
160
 
@@ -84,6 +170,9 @@ end
84
170
  Prependers.load_paths(File.expand_path('app/prependers'))
85
171
  ```
86
172
 
173
+ Note that, in order for autoprepending to work, the paths of your prependers must match the names
174
+ of the prependers you defined.
175
+
87
176
  You can pass multiple arguments to `#load_paths`, which is useful if you have subdirectories in
88
177
  `app/prependers`:
89
178
 
@@ -95,37 +184,14 @@ Prependers.load_paths(
95
184
  )
96
185
  ```
97
186
 
98
- Note that, in order for autoprepending to work, the paths of your prependers must match the names
99
- of the prependers you defined.
100
-
101
- ### Using a namespace
102
-
103
- It can be useful to have a prefix namespace for your prependers. That way, you don't have to worry
104
- about accidentally overriding any vendor modules. This is actually the recommended way to define
105
- your prependers.
106
-
107
- You can accomplish this by passing an argument when including the `Prependers::Prepender` module:
108
-
109
- ```ruby
110
- module MyApp
111
- module Animals
112
- module Dog
113
- module AddBarking
114
- include Prependers::Prepender.new(MyApp)
115
-
116
- def bark
117
- puts 'Woof!'
118
- end
119
- end
120
- end
121
- end
122
- end
123
- ```
124
-
125
- If you use autoloading, you can pass the base namespace to `#load_paths`:
187
+ You can pass the `:namespace` option to `#load_paths` to have it forwarded to all prependers:
126
188
 
127
189
  ```ruby
128
- Prependers.load_paths(File.expand_path('app/prependers'), namespace: MyApp)
190
+ Prependers.load_paths(
191
+ File.expand_path('app/prependers/controllers'),
192
+ File.expand_path('app/prependers/models'),
193
+ namespace: Acme,
194
+ )
129
195
  ```
130
196
 
131
197
  ### Integrating with Rails
@@ -137,8 +203,7 @@ To use prependers in your Rails app, simply create them under `app/prependers/mo
137
203
  Prependers.setup_for_rails
138
204
  ```
139
205
 
140
- If you want to use a namespace, just pass the `:namespace` option to `#setup_for_rails` and name
141
- your files and modules accordingly.
206
+ `#setup_for_rails` accepts the same options as `#load_paths`.
142
207
 
143
208
  ## Development
144
209
 
@@ -1,28 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  require "prependers/version"
4
6
  require "prependers/errors"
7
+ require "prependers/annotate/namespace"
8
+ require "prependers/annotate/verify"
5
9
  require "prependers/prepender"
6
10
  require "prependers/loader"
7
11
 
8
12
  module Prependers
9
- class << self
10
- def load_paths(*paths, **options)
11
- paths.flatten.each do |path|
12
- Loader.new(path, options).load
13
- end
13
+ def self.load_paths(*paths, **options)
14
+ paths.flatten.each do |path|
15
+ Loader.new(path, options).load
14
16
  end
17
+ end
15
18
 
16
- def setup_for_rails(load_options = {})
17
- prependers_directories = Rails.root.join('app', 'prependers').glob('*')
19
+ def self.setup_for_rails(load_options = {})
20
+ prependers_directories = Rails.root.join('app', 'prependers').glob('*')
18
21
 
19
- Rails.application.config.tap do |config|
20
- config.autoload_paths += prependers_directories
22
+ Rails.application.config.tap do |config|
23
+ config.autoload_paths += prependers_directories
21
24
 
22
- config.to_prepare do
23
- Prependers.load_paths(prependers_directories, load_options)
24
- end
25
+ config.to_prepare do
26
+ Prependers.load_paths(prependers_directories, load_options)
25
27
  end
26
28
  end
27
29
  end
30
+
31
+ def self.prependable_for(prepender)
32
+ prependable = prepender.name.split('::')[0..-2].join('::')
33
+
34
+ if prepender.respond_to?(:__prependers_namespace__)
35
+ prependable = (prependable[(prepender.__prependers_namespace__.name.length + 2)..-1]).to_s
36
+ end
37
+
38
+ Object.const_get(prependable)
39
+ end
28
40
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prependers
4
+ module Annotate
5
+ class Namespace < Module
6
+ attr_reader :namespace
7
+
8
+ def self.[](namespace)
9
+ new(namespace)
10
+ end
11
+
12
+ def initialize(namespace)
13
+ @namespace = namespace
14
+ end
15
+
16
+ def included(base)
17
+ base.singleton_class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
18
+ def __prependers_namespace__
19
+ #{@namespace}
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prependers
4
+ module Annotate
5
+ class Verify < Module
6
+ WRONG_HASH_ERROR = \
7
+ "The stored hash for %{prepended_module} in %{prepender} is %{stored_hash}, but the " \
8
+ "current hash is %{current_hash} instead.\n\n" \
9
+ "This most likely means that the original source has changed.\n\n" \
10
+ "Check that your prepender is still valid, then update the stored hash:\n\n" \
11
+ " include Prependers::Prepender[verify: '%{current_hash}']"
12
+
13
+ UNSET_HASH_ERROR = \
14
+ "You have not defined an original hash for %{prepended_module} in %{prepender}.\n\n" \
15
+ "You can define the hash by updating your include statement as follows:\n\n" \
16
+ " include Prependers::Prepender[verify: '%{current_hash}']"
17
+
18
+ attr_reader :original_hash
19
+
20
+ def self.[](original_hash)
21
+ new(original_hash)
22
+ end
23
+
24
+ def initialize(original_hash)
25
+ @original_hash = original_hash
26
+ end
27
+
28
+ def included(prepender)
29
+ prependable = Prependers.prependable_for(prepender)
30
+ current_hash = compute_hash(prependable)
31
+
32
+ return if current_hash == original_hash
33
+
34
+ error = (original_hash ? WRONG_HASH_ERROR : UNSET_HASH_ERROR) % {
35
+ prepended_module: prependable,
36
+ prepender: prepender.name,
37
+ stored_hash: original_hash,
38
+ current_hash: current_hash,
39
+ }
40
+
41
+ raise OutdatedPrependerError, error
42
+ end
43
+
44
+ private
45
+
46
+ def compute_hash(constant)
47
+ methods = constant.methods(false).map(&constant.method(:method)) +
48
+ constant.instance_methods(false).map(&constant.method(:instance_method))
49
+
50
+ raise NameError, "#{constant} has no methods" if methods.empty?
51
+
52
+ source_locations = methods.map(&:source_location).compact.map(&:first).uniq.sort
53
+
54
+ contents = source_locations.map { |path| File.read(path) }.join
55
+
56
+ Digest::SHA1.hexdigest(contents)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,4 +4,6 @@ module Prependers
4
4
  class Error < StandardError; end
5
5
 
6
6
  class NoPrependerError < Error; end
7
+
8
+ class OutdatedPrependerError < Error; end
7
9
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Prependers
4
4
  class Loader
5
+ UNDEFINED_PREPENDER_ERROR = \
6
+ "Expected `%{path}` to define `%{prepender}`, but it is not defined.\n\n" \
7
+ "This is most likely because the file has not been required.\n\n" \
8
+ "Require the file yourself before calling `Prependers.load_paths`."
9
+
5
10
  attr_reader :base_path, :options
6
11
 
7
12
  def initialize(base_path, options = {})
@@ -14,21 +19,20 @@ module Prependers
14
19
  absolute_path = Pathname.new(File.expand_path(path))
15
20
  relative_path = absolute_path.relative_path_from(base_path)
16
21
 
17
- prepender_module_name = expected_module_for(relative_path)
22
+ prepender_name = expected_module_for(relative_path)
18
23
 
19
- unless Object.const_defined?(prepender_module_name)
20
- error = <<~ERROR
21
- Expected #{absolute_path} to define #{prepender_module_name}, but module is not defined.
24
+ unless Object.const_defined?(prepender_name)
25
+ raise NoPrependerError, UNDEFINED_PREPENDER_ERROR % {
26
+ path: absolute_path,
27
+ prepender: prepender_name,
28
+ }
29
+ end
22
30
 
23
- Note that Prependers does not require files automatically - you will have to do that
24
- yourself before calling `#load_paths`.
25
- ERROR
31
+ prepender = Object.const_get(prepender_name)
26
32
 
27
- raise NoPrependerError, error
33
+ if prepender.ancestors.none? { |ancestor| ancestor.is_a?(Prependers::Prepender) }
34
+ prepender.include(Prepender.new(options))
28
35
  end
29
-
30
- prepender_module = Object.const_get(prepender_module_name)
31
- prepender_module.include Prepender.new(options[:namespace])
32
36
  end
33
37
  end
34
38
 
@@ -2,26 +2,40 @@
2
2
 
3
3
  module Prependers
4
4
  class Prepender < Module
5
- CLASS_METHODS_MODULE_NAME = 'ClassMethods'
5
+ def self.[](options = {})
6
+ new(options)
7
+ end
8
+
9
+ attr_reader :options
10
+
11
+ NAMESPACE_DEPRECATION = \
12
+ "[DEPRECATION] Passing a namespace to `Prependers::Prepender#[]` is deprecated. Use the " \
13
+ "`:namespace` option instead."
6
14
 
7
- attr_reader :namespace
15
+ def initialize(options_or_namespace = {})
16
+ if options_or_namespace.is_a?(Module)
17
+ warn NAMESPACE_DEPRECATION % { namespace: options_or_namespace }
18
+ options_or_namespace = { namespace: options_or_namespace }
19
+ end
8
20
 
9
- def initialize(namespace = nil)
10
- @namespace = namespace
21
+ @options = options_or_namespace
11
22
  end
12
23
 
13
24
  def included(base)
14
- prepended_module_name = base.name.split('::')[0..-2].join('::')
25
+ if options.key?(:namespace)
26
+ base.include Prependers::Annotate::Namespace.new(options[:namespace])
27
+ end
15
28
 
16
- if namespace
17
- prepended_module_name = (prepended_module_name[(namespace.name.length + 2)..-1]).to_s
29
+ if options.key?(:verify)
30
+ base.include Prependers::Annotate::Verify.new(options[:verify])
18
31
  end
19
32
 
20
- prepended_module = Object.const_get(prepended_module_name)
21
- prepended_module.prepend base
33
+ prependable = Prependers.prependable_for(base)
34
+
35
+ prependable.prepend(base)
22
36
 
23
- if base.const_defined?(CLASS_METHODS_MODULE_NAME)
24
- prepended_module.singleton_class.prepend base.const_get(CLASS_METHODS_MODULE_NAME)
37
+ if base.const_defined?('ClassMethods')
38
+ prependable.singleton_class.prepend(base.const_get('ClassMethods'))
25
39
  end
26
40
  end
27
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prependers
4
- VERSION = "0.3.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prependers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Desantis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-24 00:00:00.000000000 Z
11
+ date: 2020-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -157,6 +157,8 @@ files:
157
157
  - bin/console
158
158
  - bin/setup
159
159
  - lib/prependers.rb
160
+ - lib/prependers/annotate/namespace.rb
161
+ - lib/prependers/annotate/verify.rb
160
162
  - lib/prependers/errors.rb
161
163
  - lib/prependers/loader.rb
162
164
  - lib/prependers/prepender.rb