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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ae73a75b482203013f89eb32515cd611d1b198b8cedeba8681032185bf5f51a
4
- data.tar.gz: 6952f8616ad0e94edab0822888d02fd881468110d0d0546ba5117ab5ec1a6d27
3
+ metadata.gz: 55f0ae6b450c3fb4363ce991624506d3155878b101b1547ba16dd30c339f93f1
4
+ data.tar.gz: 4efeba98bac400653a8ba3877da043583da29638f3e934877ae231ec8d33e15c
5
5
  SHA512:
6
- metadata.gz: 9d1afc244d40c11bb184d0bbed288eac9f2f134d64e0a3957b5f7d08b69892914ba32e764c643d89c3612256e90d74888a8b31a00ad6ec48ff62068ed9f5a4c6
7
- data.tar.gz: 22b5807a59e13c7a61bc7bebd6b91e7fac3617e2f3dc61d5119435c78a7a47eafc7540f46c00ca0b616f2f0151cce06300112b9df92e489b9481e9b65b874070
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- conventional_extensions (0.1.0)
4
+ conventional_extensions (0.2.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # ConventionalExtensions
2
2
 
3
- ConventionalExtensions autoloads extensions to an object based on a file name convention.
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 # Will load every Ruby file under `lib/post/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 the class in our extension:
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` still points to the right file and line.
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 the class we can also define class methods directly:
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` still points to the right file and line.
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` still points to right file and line.
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 a manual `load_extensions`
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 planning on setting up a base class, where you're expecting subclasses to use extensions, you can do this:
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 defines the `inherited` method to auto-call `load_extensions`
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` you end defining an object graph like this:
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 the `Post::Cool` and `Post::Mailroom` modules are here immediately loaded (via Zeitwerks file naming conventions) and included. More often than not they're never referred to again, so they're practically implicit modules, yet defined explicitly with a fair bit of DSL on top.
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, @name = klass, klass.name
6
- @directory_name = File.join path.match(/(?=\/extensions|\.rb)/).pre_match, "extensions/"
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
- @directory_name + "#{extension}.rb"
28
+ @path_format % extension.to_s
21
29
  end
22
30
 
23
31
  def load_one(extension)
24
- contents = File.read extension
25
-
26
- case
27
- when contents.match?(/\s*class #{@name}/)
28
- require extension
29
- when !$LOADED_FEATURES.include?(extension)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConventionalExtensions
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.2"
5
5
  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, caller_locations(1, 1).first.path).load(*extensions)
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.1.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-28 00:00:00.000000000 Z
11
+ date: 2022-09-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: