conventional_extensions 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +23 -17
- data/lib/conventional_extensions/loader.rb +10 -6
- data/lib/conventional_extensions/version.rb +1 -1
- data/lib/conventional_extensions.rb +1 -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: 247b8f8bd45e5773afe0f35f6cf4210321bca59d901637f460e65381e6111711
|
4
|
+
data.tar.gz: 6dcf112c67bb3ec3df18ac7ff3b8c7c77b11dc513be3da153c4b825ef8de8775
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0cbb425f5eb691dcaaec2755cdd426e4950d0c26fde845739a23e663939899eb1baeb1e4ebb66f761350ada334eb67b96e0fa929671be246c1016b4a7856f983
|
7
|
+
data.tar.gz: 54dc95e8c7bdc679192b1ca5a2bb7e2d9deb38da220036401a9b34ed6d38e48bda24a5e1db80389ab6287d1d788d89e9f97b59fd2829ebcb86ef4fcddbbaee66
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.1] - 2022-09-07
|
4
|
+
|
5
|
+
- Fixes `extend ConventionalExtensions.load_on_inherited` not finding the right directory to load extensions from. See https://github.com/kaspth/conventional_extensions/commit/1f6fe9cbf2fe44ed9d699a5dd485eaa366d875a4
|
6
|
+
|
7
|
+
## [0.2.0] - 2022-08-27
|
8
|
+
|
9
|
+
- 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
|
10
|
+
|
3
11
|
## [0.1.0] - 2022-08-27
|
4
12
|
|
5
13
|
- Initial release
|
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:
|
@@ -4,10 +4,8 @@ require "set"
|
|
4
4
|
|
5
5
|
class ConventionalExtensions::Loader
|
6
6
|
def initialize(klass, path)
|
7
|
-
@klass, @
|
8
|
-
@
|
9
|
-
|
10
|
-
@loaded = Set.new
|
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"
|
11
9
|
end
|
12
10
|
|
13
11
|
def load(*extensions)
|
@@ -16,17 +14,23 @@ class ConventionalExtensions::Loader
|
|
16
14
|
end
|
17
15
|
|
18
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
|
+
|
19
23
|
def extension_paths
|
20
24
|
Dir.glob extension_path_for("*")
|
21
25
|
end
|
22
26
|
|
23
27
|
def extension_path_for(extension)
|
24
|
-
@
|
28
|
+
@path_format % extension.to_s
|
25
29
|
end
|
26
30
|
|
27
31
|
def load_one(extension)
|
28
32
|
if @loaded.add?(extension)
|
29
|
-
if contents = File.read(extension) and contents.match?(
|
33
|
+
if contents = File.read(extension) and contents.match?(@matcher)
|
30
34
|
::Kernel.load extension
|
31
35
|
else
|
32
36
|
@klass.class_eval contents, extension, 0
|
@@ -6,9 +6,7 @@ module ConventionalExtensions
|
|
6
6
|
Object.extend self # We're enriching object itself, so any object can call `load_extensions`.
|
7
7
|
|
8
8
|
def load_extensions(*extensions)
|
9
|
-
loader_defined_before_entrance = defined?(@loader)
|
10
|
-
|
11
|
-
@loader ||= Loader.new(self, caller_locations(1, 1).first.path)
|
9
|
+
@loader = Loader.new(self, Object.const_source_location(name).first) unless loader_defined_before_entrance = defined?(@loader)
|
12
10
|
@loader.load(*extensions)
|
13
11
|
ensure
|
14
12
|
@loader = nil unless loader_defined_before_entrance
|
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.2.
|
4
|
+
version: 0.2.1
|
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-
|
11
|
+
date: 2022-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|