conventional_extensions 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|