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 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: