conventional_extensions 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7ae73a75b482203013f89eb32515cd611d1b198b8cedeba8681032185bf5f51a
4
+ data.tar.gz: 6952f8616ad0e94edab0822888d02fd881468110d0d0546ba5117ab5ec1a6d27
5
+ SHA512:
6
+ metadata.gz: 9d1afc244d40c11bb184d0bbed288eac9f2f134d64e0a3957b5f7d08b69892914ba32e764c643d89c3612256e90d74888a8b31a00ad6ec48ff62068ed9f5a4c6
7
+ data.tar.gz: 22b5807a59e13c7a61bc7bebd6b91e7fac3617e2f3dc61d5119435c78a7a47eafc7540f46c00ca0b616f2f0151cce06300112b9df92e489b9481e9b65b874070
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-08-27
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in conventional_extensions.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ conventional_extensions (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.16.3)
10
+ rake (13.0.6)
11
+
12
+ PLATFORMS
13
+ arm64-darwin-20
14
+ x86_64-linux
15
+
16
+ DEPENDENCIES
17
+ conventional_extensions!
18
+ minitest (~> 5.0)
19
+ rake (~> 13.0)
20
+
21
+ BUNDLED WITH
22
+ 2.3.21
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Kasper Timm Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # ConventionalExtensions
2
+
3
+ ConventionalExtensions autoloads extensions to an object based on a file name convention.
4
+
5
+ ```ruby
6
+ # lib/post.rb
7
+ class Post < SomeSuperclass
8
+ load_extensions # Will load every Ruby file under `lib/post/extensions`.
9
+ end
10
+ ```
11
+
12
+ ### Defining an extension
13
+
14
+ Since the loading above happens after the `Post` constant has been defined, we can reopen the class in our extension:
15
+
16
+ ```ruby
17
+ # lib/post/extensions/mailroom.rb
18
+ class Post # <- Post is reopened here and so there's no superclass mismatch error
19
+ def mailroom
20
+ puts "you've got mail"
21
+ end
22
+ end
23
+ ```
24
+
25
+ Now, `Post.new.mailroom` works and `Post.instance_method(:mailroom).source_location` still points to the right file and line.
26
+
27
+ #### Defining a class method in an extension
28
+
29
+ Since we're reopening the class we can also define class methods directly:
30
+
31
+ ```ruby
32
+ # lib/post/extensions/cool.rb
33
+ class Post
34
+ def self.cool
35
+ puts "really cool"
36
+ end
37
+ end
38
+ ```
39
+
40
+ Now, `Post.cool` works and `Post.method(:cool).source_location` still points to the right file and line.
41
+
42
+ Note, any class method macro extensions are available within the top-level Post definition too:
43
+
44
+ ```ruby
45
+ # lib/post.rb
46
+ class Post < SomeSuperclass
47
+ load_extensions # Loads the `cool` extension…
48
+
49
+ cool # …and now we can invoke the class method macro.
50
+ end
51
+ ```
52
+
53
+ ### Skipping class reopening boilerplate
54
+
55
+ ConventionalExtensions also supports implicit class reopening so you can skip `class Post`, like so:
56
+
57
+ ```ruby
58
+ # lib/post/extensions/mailroom.rb
59
+ def mailroom
60
+ puts "you've got mail"
61
+ end
62
+ ```
63
+
64
+ With this, `Post.new.mailroom` still works and `Post.instance_method(:mailroom).source_location` still points to right file and line.
65
+
66
+ ### Resolve dependencies with a manual `load_extensions`
67
+
68
+ In case you need to have more fine grained control over the loading, you can call `load_extensions` from within an extension:
69
+
70
+ ```ruby
71
+ # lib/post/extensions/mailroom.rb
72
+ load_extension :named
73
+ named :sup # We're depending on the class method macro from the `named` extension, and hoisting the loading.
74
+
75
+ def mailroom
76
+ …
77
+ end
78
+
79
+ # lib/post/extensions/named.rb
80
+ def self.named(key)
81
+ puts key
82
+ end
83
+ ```
84
+
85
+ ### Supports `# frozen_string_literal: true`
86
+
87
+ Whether extensions use explicit or implicit class reopening, `# frozen_string_literal: true` is supported.
88
+
89
+ ### Providing a base class that expects ConventionalExtensions loading
90
+
91
+ In case you're planning on setting up a base class, where you're expecting subclasses to use extensions, you can do this:
92
+
93
+ ```ruby
94
+ class BaseClass
95
+ extend ConventionalExtensions.load_on_inherited # This defines the `inherited` method to auto-call `load_extensions`
96
+ end
97
+
98
+ class Subclass < BaseClass
99
+ # No need to write `load_extensions` here
100
+ end
101
+ ```
102
+
103
+ ## A less boilerplate heavy alternative to `ActiveSupport::Concern` for Active Records
104
+
105
+ Typically, when writing an app domain model with `ActiveSupport::Concern` you end defining an object graph like this:
106
+
107
+ ```ruby
108
+ # app/models/post.rb
109
+ class Post < ApplicationRecord
110
+ include Cool, Mailroom
111
+ end
112
+
113
+ # app/models/post/cool.rb
114
+ module Post::Cool
115
+ extend ActiveSupport::Concern
116
+
117
+ class_methods do
118
+ def cool
119
+ puts "really cool"
120
+ end
121
+ end
122
+ end
123
+
124
+ # app/models/post/mailroom.rb
125
+ module Post::Mailroom
126
+ extend ActiveSupport::Concern
127
+
128
+ included do
129
+ belongs_to :creator, class_name: "User"
130
+ end
131
+
132
+ def mailroom
133
+ puts "you've got mail"
134
+ end
135
+ end
136
+ ```
137
+
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.
139
+
140
+ With ConventionalExtensions you'd write this instead:
141
+
142
+ ```ruby
143
+ # app/models/post.rb
144
+ class Post < ApplicationRecord # ConventionalExtensions automatically loads extensions for Active Record models.
145
+ end
146
+
147
+ # app/models/post/extensions/cool.rb
148
+ class Post
149
+ def self.cool
150
+ puts "really cool"
151
+ end
152
+ end
153
+
154
+ # app/models/post/extensions/mailroom.rb
155
+ class Post
156
+ belongs_to :creator, class_name: "User"
157
+
158
+ def mailroom
159
+ puts "you've got mail"
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## Installation
165
+
166
+ Install the gem and add to the application's Gemfile by executing:
167
+
168
+ $ bundle add conventional_extensions
169
+
170
+ If bundler is not being used to manage dependencies, install the gem by executing:
171
+
172
+ $ gem install conventional_extensions
173
+
174
+ ## Development
175
+
176
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
177
+
178
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
179
+
180
+ ## Contributing
181
+
182
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kaspth/conventional_extensions.
183
+
184
+ ## License
185
+
186
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/conventional_extensions/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "conventional_extensions"
7
+ spec.version = ConventionalExtensions::VERSION
8
+ spec.authors = ["Kasper Timm Hansen"]
9
+ spec.email = ["hey@kaspth.com"]
10
+
11
+ spec.summary = "ConventionalExtensions sets up a file naming convention to extend your domain model"
12
+ spec.homepage = "https://github.com/kaspth/conventional_extensions"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ConventionalExtensions::Loader
4
+ def initialize(klass, path)
5
+ @klass, @name = klass, klass.name
6
+ @directory_name = File.join path.match(/(?=\/extensions|\.rb)/).pre_match, "extensions/"
7
+ end
8
+
9
+ def load(*extensions)
10
+ paths = extensions.empty? ? extension_paths : extensions.map { extension_path_for _1 }
11
+ paths.each { load_one _1 }
12
+ end
13
+
14
+ private
15
+ def extension_paths
16
+ Dir.glob extension_path_for("*")
17
+ end
18
+
19
+ def extension_path_for(extension)
20
+ @directory_name + "#{extension}.rb"
21
+ end
22
+
23
+ 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
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConventionalExtensions
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conventional_extensions/version"
4
+
5
+ module ConventionalExtensions
6
+ Object.extend self # We're enriching object itself, so any object can call `load_extensions`.
7
+
8
+ def load_extensions(*extensions)
9
+ Loader.new(self, caller_locations(1, 1).first.path).load(*extensions)
10
+ end
11
+ alias load_extension load_extensions
12
+
13
+ # extend ConventionalExtensions.load_on_inherited
14
+ def self.load_on_inherited() = LoadOnInherited
15
+
16
+ module LoadOnInherited
17
+ def inherited(klass)
18
+ super
19
+ klass.load_extensions
20
+ end
21
+ end
22
+
23
+ autoload :Loader, "conventional_extensions/loader"
24
+ end
25
+
26
+ defined?(ActiveSupport.on_load) and ActiveSupport.on_load(:active_record) { extend ConventionalExtensions.load_on_inherited }
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: conventional_extensions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kasper Timm Hansen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-08-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - hey@kaspth.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - Gemfile
22
+ - Gemfile.lock
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - conventional_extensions.gemspec
27
+ - lib/conventional_extensions.rb
28
+ - lib/conventional_extensions/loader.rb
29
+ - lib/conventional_extensions/version.rb
30
+ homepage: https://github.com/kaspth/conventional_extensions
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/kaspth/conventional_extensions
35
+ source_code_uri: https://github.com/kaspth/conventional_extensions
36
+ changelog_uri: https://github.com/kaspth/conventional_extensions/blob/main/CHANGELOG.md
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 3.0.0
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.3.21
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: ConventionalExtensions sets up a file naming convention to extend your domain
56
+ model
57
+ test_files: []