conventional_extensions 0.1.0 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +23 -17
- data/lib/conventional_extensions/loader.rb +17 -11
- data/lib/conventional_extensions/version.rb +1 -1
- data/lib/conventional_extensions.rb +12 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55f0ae6b450c3fb4363ce991624506d3155878b101b1547ba16dd30c339f93f1
|
4
|
+
data.tar.gz: 4efeba98bac400653a8ba3877da043583da29638f3e934877ae231ec8d33e15c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 023a7134b14d0c20670fb548f4466290278f403ac9cd374b3c8b6a1ab8f58b9f8d9c7c502f599469ce8a4aeeb3288599bcb8c95b773b9b63f9c36e978a61b8d2
|
7
|
+
data.tar.gz: 59b476799cbe313d539b2802420b4fc938e5628dad7e59cc9f258396dcaf140d553d6db6d2c042bbd6f8c1670b3629bcc65e7c02e07e98eb817e0dc164187c54
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.2] - 2022-09-08
|
4
|
+
|
5
|
+
- Fixes Zeitwerk 2.6.0 compatibility issue where extensions wouldn't load and throw an exception. See https://github.com/kaspth/conventional_extensions/commit/7fc25f1e860637e8df9fd0e81e7ca038c8c34aa0
|
6
|
+
|
7
|
+
## [0.2.1] - 2022-09-07
|
8
|
+
|
9
|
+
- Fixes `extend ConventionalExtensions.load_on_inherited` not finding the right directory to load extensions from. See https://github.com/kaspth/conventional_extensions/commit/1f6fe9cbf2fe44ed9d699a5dd485eaa366d875a4
|
10
|
+
|
11
|
+
## [0.2.0] - 2022-08-27
|
12
|
+
|
13
|
+
- Replaces the use of `::Kernel.require` with `::Kernel.load` and replaces the `$LOADED_FEATURES` tracking with an internally maintained `Set` for better Zeitwerk (Rails autoloading) inter op. See https://github.com/kaspth/conventional_extensions/pull/2
|
14
|
+
|
3
15
|
## [0.1.0] - 2022-08-27
|
4
16
|
|
5
17
|
- Initial release
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# ConventionalExtensions
|
2
2
|
|
3
|
-
ConventionalExtensions
|
3
|
+
ConventionalExtensions allows splitting up class definitions based on convention, similar to `ActiveSupport::Concern`'s use.
|
4
|
+
|
5
|
+
The entry point is to call `load_extensions` right after a class is originally defined:
|
4
6
|
|
5
7
|
```ruby
|
6
8
|
# lib/post.rb
|
7
9
|
class Post < SomeSuperclass
|
8
|
-
load_extensions #
|
10
|
+
load_extensions # Loads every Ruby file under `lib/post/extensions/*.rb`.
|
9
11
|
end
|
10
12
|
```
|
11
13
|
|
12
14
|
### Defining an extension
|
13
15
|
|
14
|
-
Since the loading above happens after the `Post` constant has been defined, we can reopen
|
16
|
+
Since the loading above happens after the `Post` constant has been defined, we can reopen `Post` in an extension:
|
15
17
|
|
16
18
|
```ruby
|
17
19
|
# lib/post/extensions/mailroom.rb
|
@@ -22,11 +24,11 @@ class Post # <- Post is reopened here and so there's no superclass mismatch erro
|
|
22
24
|
end
|
23
25
|
```
|
24
26
|
|
25
|
-
Now, `Post.new.mailroom` works and `Post.instance_method(:mailroom).source_location`
|
27
|
+
Now, `Post.new.mailroom` works and `Post.instance_method(:mailroom).source_location` points to the extension file and line.
|
26
28
|
|
27
29
|
#### Defining a class method in an extension
|
28
30
|
|
29
|
-
Since we're reopening
|
31
|
+
Since we're reopening `Post` we can also define class methods directly:
|
30
32
|
|
31
33
|
```ruby
|
32
34
|
# lib/post/extensions/cool.rb
|
@@ -37,9 +39,9 @@ class Post
|
|
37
39
|
end
|
38
40
|
```
|
39
41
|
|
40
|
-
Now, `Post.cool` works and `Post.method(:cool).source_location`
|
42
|
+
Now, `Post.cool` works and `Post.method(:cool).source_location` points to the extension file and line.
|
41
43
|
|
42
|
-
Note, any class method macro extensions are available within the top-level Post definition too:
|
44
|
+
Note, any class method macro extensions are now available within the top-level `Post` definition too:
|
43
45
|
|
44
46
|
```ruby
|
45
47
|
# lib/post.rb
|
@@ -52,7 +54,7 @@ end
|
|
52
54
|
|
53
55
|
### Skipping class reopening boilerplate
|
54
56
|
|
55
|
-
ConventionalExtensions also supports implicit class reopening so you can skip `class Post`, like so:
|
57
|
+
ConventionalExtensions also supports implicit class reopening by automatically using `Post.class_eval` so you can skip `class Post`, like so:
|
56
58
|
|
57
59
|
```ruby
|
58
60
|
# lib/post/extensions/mailroom.rb
|
@@ -61,16 +63,16 @@ def mailroom
|
|
61
63
|
end
|
62
64
|
```
|
63
65
|
|
64
|
-
With this, `Post.new.mailroom` still works and `Post.instance_method(:mailroom).source_location`
|
66
|
+
With this, `Post.new.mailroom` still works and `Post.instance_method(:mailroom).source_location` points to the extension file and line.
|
65
67
|
|
66
|
-
### Resolve dependencies with
|
68
|
+
### Resolve dependencies with load hoisting
|
67
69
|
|
68
|
-
In case you need to have more fine grained control over the loading, you can call `load_extensions` from within an extension:
|
70
|
+
In case you need to have more fine grained control over the loading, you can call `load_extensions` or `load_extension` from within an extension:
|
69
71
|
|
70
72
|
```ruby
|
71
73
|
# lib/post/extensions/mailroom.rb
|
72
74
|
load_extension :named
|
73
|
-
named :sup # We're depending on the class method macro from the `named` extension, and hoisting the loading.
|
75
|
+
named :sup # We're depending on the `named` class method macro from the `named` extension, and hoisting the loading.
|
74
76
|
|
75
77
|
def mailroom
|
76
78
|
…
|
@@ -88,21 +90,21 @@ Whether extensions use explicit or implicit class reopening, `# frozen_string_li
|
|
88
90
|
|
89
91
|
### Providing a base class that expects ConventionalExtensions loading
|
90
92
|
|
91
|
-
In case you're
|
93
|
+
In case you're setting up a base class, where you're expecting subclasses to use extensions, you can do:
|
92
94
|
|
93
95
|
```ruby
|
94
96
|
class BaseClass
|
95
|
-
extend ConventionalExtensions.load_on_inherited # This
|
97
|
+
extend ConventionalExtensions.load_on_inherited # This calls `load_extensions` automatically in the `inherited` hook.
|
96
98
|
end
|
97
99
|
|
98
100
|
class Subclass < BaseClass
|
99
|
-
# No need to write `load_extensions` here
|
101
|
+
# No need to write `load_extensions` here, it's called already.
|
100
102
|
end
|
101
103
|
```
|
102
104
|
|
103
105
|
## A less boilerplate heavy alternative to `ActiveSupport::Concern` for Active Records
|
104
106
|
|
105
|
-
Typically, when writing an app domain model with `ActiveSupport::Concern`
|
107
|
+
Typically, when writing an app domain model with `ActiveSupport::Concern` your object graph looks like this:
|
106
108
|
|
107
109
|
```ruby
|
108
110
|
# app/models/post.rb
|
@@ -135,7 +137,7 @@ module Post::Mailroom
|
|
135
137
|
end
|
136
138
|
```
|
137
139
|
|
138
|
-
Both
|
140
|
+
Both `Post::Cool` and `Post::Mailroom` are immediately loaded (via Zeitwerk's file naming conventions) & included. Most often these concern modules are never referred to again, so they're practically implicit modules, yet defined with tricky DSL.
|
139
141
|
|
140
142
|
With ConventionalExtensions you'd write this instead:
|
141
143
|
|
@@ -161,6 +163,10 @@ class Post
|
|
161
163
|
end
|
162
164
|
```
|
163
165
|
|
166
|
+
There are places where concerns are more suited:
|
167
|
+
* Multi-model concerns in `app/models/concerns`, you'd need modules to help with that.
|
168
|
+
* Needing to include multiple levels of modules and have them all inserted directly on the base class, concerns have this built in, but ConventionalExtensions can't support that. It's a rare use case nonetheless.
|
169
|
+
|
164
170
|
## Installation
|
165
171
|
|
166
172
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
class ConventionalExtensions::Loader
|
4
6
|
def initialize(klass, path)
|
5
|
-
@klass, @
|
6
|
-
@
|
7
|
+
@loaded, @klass, @matcher = Set.new, klass, /\s*class #{klass.name}/
|
8
|
+
@path_format = File.join File.dirname(path), underscore(klass.name), "extensions", "%s.rb"
|
7
9
|
end
|
8
10
|
|
9
11
|
def load(*extensions)
|
@@ -12,23 +14,27 @@ class ConventionalExtensions::Loader
|
|
12
14
|
end
|
13
15
|
|
14
16
|
private
|
17
|
+
# Logic borrowed from Active Support:
|
18
|
+
# https://github.com/rails/rails/blob/a2fc96a80cf26c11df3e86e86c1b2b61736af80c/activesupport/lib/active_support/inflector/methods.rb#L99
|
19
|
+
def underscore(name)
|
20
|
+
name.gsub("::", "/").tap { _1.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" } }.tap(&:downcase!)
|
21
|
+
end
|
22
|
+
|
15
23
|
def extension_paths
|
16
24
|
Dir.glob extension_path_for("*")
|
17
25
|
end
|
18
26
|
|
19
27
|
def extension_path_for(extension)
|
20
|
-
@
|
28
|
+
@path_format % extension.to_s
|
21
29
|
end
|
22
30
|
|
23
31
|
def load_one(extension)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
$LOADED_FEATURES << extension
|
31
|
-
@klass.class_eval contents, extension, 0
|
32
|
+
if @loaded.add?(extension)
|
33
|
+
if contents = File.read(extension) and contents.match?(@matcher)
|
34
|
+
::Kernel.load extension
|
35
|
+
else
|
36
|
+
@klass.class_eval contents, extension, 0
|
37
|
+
end
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
@@ -5,8 +5,11 @@ require_relative "conventional_extensions/version"
|
|
5
5
|
module ConventionalExtensions
|
6
6
|
Object.extend self # We're enriching object itself, so any object can call `load_extensions`.
|
7
7
|
|
8
|
-
def load_extensions(*extensions)
|
9
|
-
Loader.new(self,
|
8
|
+
def load_extensions(*extensions, from: Frame.previous.path)
|
9
|
+
@loader = Loader.new(self, from) unless loader_defined_before_entrance = defined?(@loader)
|
10
|
+
@loader.load(*extensions)
|
11
|
+
ensure
|
12
|
+
@loader = nil unless loader_defined_before_entrance
|
10
13
|
end
|
11
14
|
alias load_extension load_extensions
|
12
15
|
|
@@ -16,7 +19,13 @@ module ConventionalExtensions
|
|
16
19
|
module LoadOnInherited
|
17
20
|
def inherited(klass)
|
18
21
|
super
|
19
|
-
klass.load_extensions
|
22
|
+
klass.load_extensions from: Frame.previous.path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Frame
|
27
|
+
def self.previous
|
28
|
+
caller_locations(2, 1).first # Use 2 instead of 1 so we get the frame of who called us.
|
20
29
|
end
|
21
30
|
end
|
22
31
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: conventional_extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kasper Timm Hansen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-08
|
11
|
+
date: 2022-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|