prependers 0.3.0 → 1.0.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: 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