inheritance-helper 0.2.5 → 0.2.6

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: 562ea69d6c46e724638823857ec7df66ec87408b00c541cd63ce34f69fd9f1e7
4
- data.tar.gz: 806d2e7ba749dd237e4588794d393aaaf29b76b9c54f41d7174335145d51326b
3
+ metadata.gz: 31680a3ae280b81619d4eef1777f9821a4c47ef390225e56a4805b9686747fb9
4
+ data.tar.gz: 998ec788e4497c7e90870ece2d008dc76226344d7d24c0be408b7e6d4b859673
5
5
  SHA512:
6
- metadata.gz: 9cb15000be0f56dd9a8fb48881d7766a07aee34d4330a67c049cbb8370646cccdf4d3a3d6c63f321b18dca91b9bd89ae103a6c9d0f93b339c193f3292a498d95
7
- data.tar.gz: 865ea2dbce6c43fdd02424e6936bf385d14e61e5ca5b8904a35f7e628bf142cc2ed332c6d66235789330839522cef3e1d8379b7d37a8e5154b069bb92637de4f
6
+ metadata.gz: 47fc914a0bae8fedf35b01b0c9f4db8db002b1b1368d9bdaaf9b13a9ad537a5fc94a84900d652515874e6597acbd76f6d366bf50e2da6c798f9b756f89059333
7
+ data.tar.gz: 96f5b2c69b69ef9a8720b9c5db7f64aeb6cfb578516c2586a4bc57085b91700e393090872f986ebfbbd598ba26eb1578f7ec182fede9272e5bb887fa0362de8c
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.2.6](https://github.com/dougyouch/inheritance-helper/compare/v0.2.5...v0.2.6) (2026-10-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **class-builder:** handle repeated and trailing underscores in class names ([92488f2](https://github.com/dougyouch/inheritance-helper/commit/92488f260e9da03693745c46c9064cafb1f2159f))
9
+ * **methods:** keep the visibility of redefined class methods ([5cd1f79](https://github.com/dougyouch/inheritance-helper/commit/5cd1f79529de471c27249a163b6de66cc45d9453))
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Douglas Youch
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # inheritance-helper
2
+
3
+ Class-level settings that subclasses can extend without changing their parents.
4
+
5
+ [![CI](https://github.com/dougyouch/inheritance-helper/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dougyouch/inheritance-helper/actions/workflows/ci.yml)
6
+ [![Coverage](https://raw.githubusercontent.com/dougyouch/inheritance-helper/badges/coverage.svg)](https://github.com/dougyouch/inheritance-helper/actions/workflows/ci.yml)
7
+ [![Branch Coverage](https://raw.githubusercontent.com/dougyouch/inheritance-helper/badges/branches.svg)](https://github.com/dougyouch/inheritance-helper/actions/workflows/ci.yml)
8
+ [![Gem Version](https://badge.fury.io/rb/inheritance-helper.svg)](https://rubygems.org/gems/inheritance-helper)
9
+
10
+ [API reference](https://rubydoc.info/gems/inheritance-helper) · [Changelog](CHANGELOG.md)
11
+
12
+ DSLs often collect data at the class level: attributes, options, callbacks. With a class instance variable,
13
+ subclasses don't see the parent's data. With a class variable (`@@attributes`), every class in the chain
14
+ shares and changes the same object. `inheritance-helper` avoids both: each call replaces the class method on
15
+ the class it's called on, so a subclass starts with its parent's value, adds to it, and the parent keeps its
16
+ own.
17
+
18
+ It has no dependencies. It is used by [schema-model](https://github.com/dougyouch/schema) to collect
19
+ attribute definitions.
20
+
21
+ ## Installation
22
+
23
+ Requires Ruby 3.2 or newer. Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'inheritance-helper'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ ```bash
32
+ $ bundle install
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Extend a class with `InheritanceHelper::Methods` and define a class method that returns the starting value.
38
+ Freezing the value is recommended: values built from a frozen value are frozen too, so no class can change
39
+ another's data by mutating it.
40
+
41
+ ```ruby
42
+ require 'inheritance-helper'
43
+
44
+ class Model
45
+ extend InheritanceHelper::Methods
46
+
47
+ def self.attributes
48
+ {}.freeze
49
+ end
50
+
51
+ def self.attribute(name, type)
52
+ add_value_to_class_method(:attributes, name => type)
53
+ attr_accessor name
54
+ end
55
+ end
56
+
57
+ class Person < Model
58
+ attribute :name, :string
59
+ attribute :phone, :string
60
+ end
61
+
62
+ class Employee < Person
63
+ attribute :employee_id, :integer
64
+ end
65
+
66
+ Model.attributes # => {}
67
+ Person.attributes # => {name: :string, phone: :string}
68
+ Employee.attributes # => {name: :string, phone: :string, employee_id: :integer}
69
+ ```
70
+
71
+ ### add_value_to_class_method
72
+
73
+ Redefines a class method to return its current value with more added:
74
+
75
+ - a **Hash** is merged with the new hash
76
+ - an **Array** or **Set** is combined with `Array(value)`, so an array of values adds each one and the
77
+ result stays flat
78
+
79
+ ```ruby
80
+ class Base
81
+ extend InheritanceHelper::Methods
82
+
83
+ def self.fields = [].freeze
84
+ def self.tags = Set.new.freeze
85
+ end
86
+
87
+ class Child < Base
88
+ add_value_to_class_method :fields, :name
89
+ add_value_to_class_method :fields, %i[email phone]
90
+ add_value_to_class_method :tags, :admin
91
+ end
92
+
93
+ Child.fields # => [:name, :email, :phone]
94
+ Child.tags # => Set[:admin]
95
+ Base.fields # => []
96
+ ```
97
+
98
+ ### append_value_to_class_method
99
+
100
+ Redefines a class method to return its current array with one element appended. Unlike
101
+ `add_value_to_class_method`, an array is added as a single element:
102
+
103
+ ```ruby
104
+ class Base
105
+ extend InheritanceHelper::Methods
106
+
107
+ def self.callbacks = [].freeze
108
+
109
+ def self.before_save(*methods)
110
+ append_value_to_class_method(:callbacks, methods)
111
+ end
112
+ end
113
+
114
+ class Child < Base
115
+ before_save :normalize, :validate
116
+ before_save :log
117
+ end
118
+
119
+ Child.callbacks # => [[:normalize, :validate], [:log]]
120
+ ```
121
+
122
+ ### redefine_class_method
123
+
124
+ Replaces a class method with one that returns the given value. Use it for settings that are replaced rather
125
+ than added to:
126
+
127
+ ```ruby
128
+ class Base
129
+ extend InheritanceHelper::Methods
130
+
131
+ def self.table_name = 'records'
132
+
133
+ def self.table(name)
134
+ redefine_class_method(:table_name, name.freeze)
135
+ end
136
+ end
137
+
138
+ class User < Base
139
+ table 'users'
140
+ end
141
+
142
+ User.table_name # => "users"
143
+ Base.table_name # => "records"
144
+ ```
145
+
146
+ The method returns the same object on every call, so freeze values that callers shouldn't change. The new
147
+ method keeps the visibility of the one it replaces, so a private class method stays private.
148
+
149
+ `InheritanceHelper::Methods.redefine_class_method(klass, method, value)` does the same for a class that
150
+ doesn't extend the module.
151
+
152
+ ### Notes
153
+
154
+ - The class method must already return a value: `add_value_to_class_method` and
155
+ `append_value_to_class_method` call it to get the current value.
156
+ - Each call defines a method on the class's singleton class. Classes declared at load time (the usual DSL
157
+ case) are fine; redefining class methods from several threads at once is not synchronized.
158
+
159
+ ## ClassBuilder::Utils
160
+
161
+ `InheritanceHelper::ClassBuilder::Utils` creates named classes at runtime, which is useful for DSLs that
162
+ build nested classes (such as a `has_many :items do ... end` block):
163
+
164
+ ```ruby
165
+ module Shop; end
166
+
167
+ klass = InheritanceHelper::ClassBuilder::Utils.create_class(Shop, :line_item, nil, 'Schema', nil) do
168
+ attr_accessor :quantity
169
+ end
170
+
171
+ klass # => Shop::SchemaLineItem
172
+ klass.name # => "Shop::SchemaLineItem"
173
+
174
+ InheritanceHelper::ClassBuilder::Utils.get_class_name(:line_item, 'Has', 'Class')
175
+ # => "HasLineItemClass"
176
+ ```
177
+
178
+ `create_class(base_module, name, base_class, prefix, suffix, &block)` subclasses `base_class` (or `Object` when
179
+ `nil`), assigns it to the constant in `base_module`, and evaluates the block in the new class. If the constant
180
+ already exists it is replaced, with Ruby's "already initialized constant" warning.
181
+
182
+ `get_class_name` uses `String#classify` when ActiveSupport is loaded, which also singularizes the name
183
+ (`line_items` becomes `LineItem`). Without ActiveSupport the name is split on underscores and each part is
184
+ capitalized (`line_items` becomes `LineItems`).
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ bundle install
190
+ bundle exec rspec # tests, with line and branch coverage in coverage/
191
+ bundle exec rubocop # lint
192
+ bundle exec yard doc # API docs in doc/
193
+ ```
194
+
195
+ CI requires 100% line and branch coverage, no RuboCop offenses, and every public method documented with YARD.
196
+
197
+ Releases are automated with [release-please](https://github.com/googleapis/release-please): conventional
198
+ commits on `master` (`fix:`, `feat:`) update a release PR, and merging it tags the release and publishes the
199
+ gem.
200
+
201
+ ## Contributing
202
+
203
+ 1. Fork the repository
204
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
205
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
206
+ 4. Push to the branch (`git push origin my-new-feature`)
207
+ 5. Create a new Pull Request
208
+
209
+ ## License
210
+
211
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -1,31 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InheritanceHelper
4
- # Utility methods for building dynamic classes
5
4
  module ClassBuilder
5
+ # Functions for creating named classes inside a module.
6
+ #
7
+ # @example
8
+ # InheritanceHelper::ClassBuilder::Utils.create_class(MyApp, :line_item, Struct.new(:id), 'Schema', nil)
9
+ # # => MyApp::SchemaLineItem
6
10
  module Utils
7
11
  module_function
8
12
 
9
- def get_class_name(name, preffix_class_name, postfix_class_name)
13
+ # Builds a class name from `name` with an optional prefix and suffix.
14
+ #
15
+ # `name` is converted with `String#classify` when ActiveSupport is loaded (which also singularizes it).
16
+ # Otherwise it is split on underscores and each part is capitalized.
17
+ #
18
+ # @param name [String, Symbol] the base name, such as `:line_item`
19
+ # @param prefix_class_name [String, nil] text to put before the converted name
20
+ # @param suffix_class_name [String, nil] text to put after the converted name
21
+ # @return [String] the class name, such as `"SchemaLineItem"`
22
+ # @example
23
+ # get_class_name(:line_item, 'Schema', 'Class') # => "SchemaLineItemClass"
24
+ def get_class_name(name, prefix_class_name, suffix_class_name)
10
25
  name = name.to_s
11
26
 
12
27
  class_name =
13
28
  if name.respond_to?(:classify)
14
29
  name.classify
15
30
  else
16
- # simple naive method to convert a string to a class name
17
- name.gsub(/(^.|_.)/) { |_| ($1[1] || $1[0]).upcase }
31
+ name.split('_').map { |part| part.sub(/\A./, &:upcase) }.join
18
32
  end
19
33
 
20
- preffix_class_name.to_s + class_name + postfix_class_name.to_s
34
+ "#{prefix_class_name}#{class_name}#{suffix_class_name}"
21
35
  end
22
36
 
23
- # Purpose to create a class that is under a module with an actual class name
24
- def create_class(base_module, name, base_class, preffix_class_name, postfix_class_name, &block)
25
- class_name = get_class_name(name, preffix_class_name, postfix_class_name)
26
- kls = base_class ? Class.new(base_class) : Class.new
37
+ # Creates a class and assigns it to a constant in `base_module`, which gives the class a name.
38
+ #
39
+ # If the constant already exists it is replaced, and Ruby prints an "already initialized constant"
40
+ # warning.
41
+ #
42
+ # @param base_module [Module] the module the class is defined in
43
+ # @param name [String, Symbol] the base name, converted with {get_class_name}
44
+ # @param base_class [Class, nil] the superclass, or `nil` for `Object`
45
+ # @param prefix_class_name [String, nil] text to put before the converted name
46
+ # @param suffix_class_name [String, nil] text to put after the converted name
47
+ # @yield evaluated in the new class with `class_eval`, to define its methods
48
+ # @return [Class] the new class
49
+ # @raise [NameError] if the resulting name isn't a valid constant name
50
+ def create_class(base_module, name, base_class, prefix_class_name, suffix_class_name, &block)
51
+ class_name = get_class_name(name, prefix_class_name, suffix_class_name)
52
+ kls = Class.new(base_class || Object)
27
53
  base_module.const_set(class_name, kls)
28
- kls = base_module.const_get(class_name)
29
54
  kls.class_eval(&block) if block
30
55
  kls
31
56
  end
@@ -1,19 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InheritanceHelper
4
- # Collection of utility methods for rewriting class methods
4
+ # Class methods for redefining class methods on a subclass.
5
+ #
6
+ # Extend a class with this module. A class method on that class, or on any subclass, can then be replaced
7
+ # with one that returns a new value. The new method is defined on the receiving class's singleton class,
8
+ # so the parent class keeps its own method and value.
9
+ #
10
+ # @example Collect attributes across an inheritance chain
11
+ # class Model
12
+ # extend InheritanceHelper::Methods
13
+ #
14
+ # def self.attributes
15
+ # {}.freeze
16
+ # end
17
+ #
18
+ # def self.attribute(name, type)
19
+ # add_value_to_class_method(:attributes, name => type)
20
+ # end
21
+ # end
22
+ #
23
+ # class Person < Model
24
+ # attribute :name, :string
25
+ # end
26
+ #
27
+ # Person.attributes # => {name: :string}
28
+ # Model.attributes # => {}
5
29
  module Methods
30
+ # Defines (or replaces) the class method `method` on `klass` so that it returns `value`.
31
+ #
32
+ # The new method keeps the visibility of the method it replaces, so a private class method stays private.
33
+ #
34
+ # @param klass [Module] the class or module to define the method on
35
+ # @param method [Symbol, String] name of the class method
36
+ # @param value [Object] the value the method returns. The same object is returned on every call, so
37
+ # freeze it if callers shouldn't change it
38
+ # @return [Module] `klass`
6
39
  def self.redefine_class_method(klass, method, value)
7
- class << klass; self; end.send(:define_method, method) { value }
40
+ singleton = klass.singleton_class
41
+ visibility = method_visibility(singleton, method)
42
+ singleton.send(:define_method, method) { value }
43
+ singleton.send(visibility, method)
8
44
  klass
9
45
  end
10
46
 
47
+ # @api private
48
+ # @param mod [Module] the module to look the method up in
49
+ # @param method [Symbol, String] name of the method
50
+ # @return [Symbol] `:private`, `:protected` or `:public` (also for a method that isn't defined yet)
51
+ def self.method_visibility(mod, method)
52
+ if mod.private_method_defined?(method)
53
+ :private
54
+ elsif mod.protected_method_defined?(method)
55
+ :protected
56
+ else
57
+ :public
58
+ end
59
+ end
60
+ private_class_method :method_visibility
61
+
62
+ # Defines (or replaces) the class method `method` on this class so that it returns `value`.
63
+ #
64
+ # @param method [Symbol, String] name of the class method
65
+ # @param value [Object] the value the method returns
66
+ # @return [Module] this class
67
+ # @see InheritanceHelper::Methods.redefine_class_method
11
68
  def redefine_class_method(method, value)
12
69
  ::InheritanceHelper::Methods.redefine_class_method(self, method, value)
13
70
  end
14
71
 
15
- # useful for adding data to hashes.
16
- # when working with arrays it keeps a flat data set
72
+ # Redefines the class method `method` to return its current value with `value` added.
73
+ #
74
+ # - A Hash is merged with `value`, which must be a Hash.
75
+ # - Anything else (an Array, a Set) is combined with `Array(value)`, so an array of values adds each one
76
+ # and the result stays flat. Use {#append_value_to_class_method} to add an array as a single element.
77
+ #
78
+ # The current value isn't changed. If it is frozen, the new value is frozen too.
79
+ #
80
+ # @param method [Symbol, String] name of a class method that returns a Hash, Array or Set
81
+ # @param value [Object] the value or values to add
82
+ # @return [Module] this class
83
+ # @example
84
+ # add_value_to_class_method(:attributes, name: :string) # {} => {name: :string}
85
+ # add_value_to_class_method(:fields, [:a, :b]) # [] => [:a, :b]
17
86
  def add_value_to_class_method(method, value)
18
87
  old_value = send(method)
19
88
 
@@ -28,7 +97,17 @@ module InheritanceHelper
28
97
  redefine_class_method(method, old_value.frozen? ? new_value.freeze : new_value)
29
98
  end
30
99
 
31
- # useful for working with arrays and maintain a list of values
100
+ # Redefines the class method `method` to return a copy of its current value with `value` appended as a
101
+ # single element (using `<<`).
102
+ #
103
+ # The current value isn't changed. If it is frozen, the new value is frozen too.
104
+ #
105
+ # @param method [Symbol, String] name of a class method that returns an Array (or anything that responds
106
+ # to `dup` and `<<`)
107
+ # @param value [Object] the element to append
108
+ # @return [Module] this class
109
+ # @example
110
+ # append_value_to_class_method(:callbacks, [:save, :log]) # [] => [[:save, :log]]
32
111
  def append_value_to_class_method(method, value)
33
112
  old_value = send(method)
34
113
  new_value = old_value.dup << value
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InheritanceHelper
4
+ # Gem version, bumped by release-please
5
+ VERSION = '0.2.6'
6
+ end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A set of utility classes for making inheritance work with class level variables
3
+ require_relative 'inheritance-helper/version'
4
+
5
+ # Helpers for class-level values that subclasses can extend without changing their parent classes.
4
6
  module InheritanceHelper
5
7
  autoload :Methods, 'inheritance-helper/methods'
6
8
 
9
+ # Helpers for creating classes at runtime.
7
10
  module ClassBuilder
8
11
  autoload :Utils, 'inheritance-helper/class_builder/utils'
9
12
  end
metadata CHANGED
@@ -1,29 +1,40 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inheritance-helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-08-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: Redefines class methods
14
- email: dougyouch+github@gmail.com
12
+ description: Redefine a class method on a subclass to return a new value, so hashes,
13
+ arrays and sets declared on a parent class can be added to by each subclass while
14
+ the parent keeps its own copy. Built for DSLs that collect attributes, options or
15
+ callbacks at the class level. Also includes a helper for creating named classes
16
+ inside a module.
17
+ email: dougyouch@gmail.com
15
18
  executables: []
16
19
  extensions: []
17
20
  extra_rdoc_files: []
18
21
  files:
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - README.md
19
25
  - lib/inheritance-helper.rb
20
26
  - lib/inheritance-helper/class_builder/utils.rb
21
27
  - lib/inheritance-helper/methods.rb
28
+ - lib/inheritance-helper/version.rb
22
29
  homepage: https://github.com/dougyouch/inheritance-helper
23
30
  licenses:
24
31
  - MIT
25
- metadata: {}
26
- post_install_message:
32
+ metadata:
33
+ rubygems_mfa_required: 'true'
34
+ source_code_uri: https://github.com/dougyouch/inheritance-helper
35
+ changelog_uri: https://github.com/dougyouch/inheritance-helper/blob/master/CHANGELOG.md
36
+ bug_tracker_uri: https://github.com/dougyouch/inheritance-helper/issues
37
+ documentation_uri: https://rubydoc.info/gems/inheritance-helper
27
38
  rdoc_options: []
28
39
  require_paths:
29
40
  - lib
@@ -31,15 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
31
42
  requirements:
32
43
  - - ">="
33
44
  - !ruby/object:Gem::Version
34
- version: '0'
45
+ version: '3.2'
35
46
  required_rubygems_version: !ruby/object:Gem::Requirement
36
47
  requirements:
37
48
  - - ">="
38
49
  - !ruby/object:Gem::Version
39
50
  version: '0'
40
51
  requirements: []
41
- rubygems_version: 3.2.3
42
- signing_key:
52
+ rubygems_version: 4.0.20
43
53
  specification_version: 4
44
- summary: Inheritance Helpers
54
+ summary: Class-level settings that subclasses can extend without changing their parents
45
55
  test_files: []