class2 0.6.1 → 0.7.0

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: 9e33244884d5c16ee2b4d8b43195e2247bfc88ad9d829cfcdaf32efc25195a72
4
- data.tar.gz: c1e550bf7044f09d66930f09a859f0bf6713e83a3f648d5fb511d0deef8ba9f1
3
+ metadata.gz: 70badfe7a9fb6e6ad1df0f513ffc6bd4a322ab559457b62c810c54949b6862f8
4
+ data.tar.gz: c9feafd17d6e2375b868b22e87f4bac8d83db1dc26b538ff7ad13fd36d71d3f0
5
5
  SHA512:
6
- metadata.gz: 9959d03761af0652265bb53e65d75614ddd47bdf0315ffe1f91d7a31396124d31b2bc3369272ebd64667b51792ef543f9ec99c800b963ed9680fc409a78d35f1
7
- data.tar.gz: 9ca603b6e2dfc257d26d8e6e31aebcc6ef08af5c46c0bfc53fdc0552a15a926c1f3a86e1fa439c55cd487ef4d0ce86c6207a101d5f44688391fe0d2d340325ae
6
+ metadata.gz: 32006d461e4e85a7c7b04c60ce9f388b670d55b3a1607afc8e2ab017b5f771aa949a411505ae7a0822c5a023865bf4e7260d79cd9997e4a73ceaa320b492bf35
7
+ data.tar.gz: ae9e57e7b101a98c2c9bece0ca8d583f6022acc81b5e293a2c0e540d8b65e128d9d2ae589c826c9aeb0fb45fb30534e43f423e9b1207eb5747f724da00ef8633
data/Changes CHANGED
@@ -1,3 +1,7 @@
1
+ 2026-09-16 v0.7.0
2
+ --------------------
3
+ * Drop ActiveSupport for inflections and use dry-inflector
4
+
1
5
  2026-09-16 v0.6.1
2
6
  --------------------
3
7
  * Add Class2::Inflector wrapper
data/README.md CHANGED
@@ -191,8 +191,7 @@ To control the creation of the top-level methods, see the
191
191
 
192
192
  #### Naming
193
193
 
194
- `class2` uses
195
- [`String#classify`](http://api.rubyonrails.org/classes/String.html#method-i-classify)
194
+ `class2` uses [`dry-inflector`](https://github.com/dry-rb/dry-inflector)
196
195
  to turn keys into class names: `:foo` will be `Foo`, `:foo_bars` will
197
196
  be `FooBar`.
198
197
 
@@ -202,7 +201,9 @@ used to derive the class names from the plural attribute names. An
202
201
  `:addresses` key with an `Array` value will result in a class named
203
202
  `Address` being created.
204
203
 
205
- Plurality is determined by [`String#pluralize`](http://api.rubyonrails.org/classes/String.html#method-i-pluralize).
204
+ Plurality is determined by
205
+ [`dry-inflector`](https://github.com/dry-rb/dry-inflector) loaded with
206
+ ActiveSupport 6.1's rules. No `String` methods are added.
206
207
 
207
208
  #### Conversions
208
209
 
data/class2.gemspec CHANGED
@@ -18,10 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activesupport", ">= 3.2", "< 9"
21
+ spec.required_ruby_version = ">= 2.7"
22
+
23
+ spec.add_dependency "dry-inflector", "~> 1.1.0"
22
24
 
23
25
  spec.add_development_dependency "bundler"
24
26
  spec.add_development_dependency "rake", "~> 13"
25
- spec.add_development_dependency "appraisal"
26
27
  spec.add_development_dependency "minitest"
27
28
  end
@@ -6,4 +6,4 @@ end
6
6
 
7
7
  source = $1
8
8
  namespace = source =~ %r{/lib/(.+?)(?:\.rb)?\z} ? $1 : File.basename(source, File.extname(source))
9
- Class2.autoload(namespace.camelize, caller.unshift(caller[0]))
9
+ Class2.autoload(Class2::Inflector.camelize(namespace), caller.unshift(caller[0]))
@@ -1,25 +1,124 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/inflector"
3
+ require "dry/inflector"
4
4
 
5
5
  class Class2
6
6
  #
7
7
  # String inflection used to convert attribute names into class names, method
8
8
  # names, and JSON keys.
9
9
  #
10
- # A lightweight wrapper around ActiveSupport::Inflector. Class2 calls this
11
- # instead of ActiveSupport's String and Module extensions so that the
12
- # inflection library can be replaced without changing the rest of Class2.
10
+ # dry-inflector provides the rule engine, but it is loaded with ActiveSupport
11
+ # 6.1's rules and case conversion so that the names Class2 generates are the
12
+ # same as when ActiveSupport provided the inflector. dry-inflector's rules,
13
+ # acronyms (API, JSON, HTTP, ...), and uncountables differ from ActiveSupport's
14
+ # and would change class names and plural attribute detection.
13
15
  #
14
16
  module Inflector
15
17
  extend self
16
18
 
19
+ # ActiveSupport's uncountables.
20
+ UNCOUNTABLE = %w[
21
+ equipment information rice money species series fish sheep jeans police
22
+ ].freeze
23
+
24
+ #
25
+ # Rules are prepended, so the last one added is the first one applied and
26
+ # they are added here in ActiveSupport's order so they have its precedence.
27
+ #
28
+ INFLECTOR = Dry::Inflector.new do |inflections|
29
+ inflections.plural(/$/, "s")
30
+ inflections.plural(/s$/i, "s")
31
+ inflections.plural(/^(ax|test)is$/i, '\1es')
32
+ inflections.plural(/(octop|vir)us$/i, '\1i')
33
+ inflections.plural(/(octop|vir)i$/i, '\1i')
34
+ inflections.plural(/(alias|status)$/i, '\1es')
35
+ inflections.plural(/(bu)s$/i, '\1ses')
36
+ inflections.plural(/(buffal|tomat)o$/i, '\1oes')
37
+ inflections.plural(/([ti])um$/i, '\1a')
38
+ inflections.plural(/([ti])a$/i, '\1a')
39
+ inflections.plural(/sis$/i, "ses")
40
+ inflections.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
41
+ inflections.plural(/(hive)$/i, '\1s')
42
+ inflections.plural(/([^aeiouy]|qu)y$/i, '\1ies')
43
+ inflections.plural(/(x|ch|ss|sh)$/i, '\1es')
44
+ inflections.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
45
+ inflections.plural(/^(m|l)ouse$/i, '\1ice')
46
+ inflections.plural(/^(m|l)ice$/i, '\1ice')
47
+ inflections.plural(/^(ox)$/i, '\1en')
48
+ inflections.plural(/^(oxen)$/i, '\1')
49
+ inflections.plural(/(quiz)$/i, '\1zes')
50
+
51
+ # ActiveSupport leaves a word alone when none of its rules match. These
52
+ # rules have a higher precedence than dry-inflector's defaults, which have
53
+ # irregulars ActiveSupport doesn't (foot/feet, tooth/teeth, goose/geese),
54
+ # so this rule -- the one with the lowest precedence -- matches everything
55
+ # and leaves it alone.
56
+ inflections.singular(/$/, '\0')
57
+ inflections.singular(/s$/i, "")
58
+ inflections.singular(/(ss)$/i, '\1')
59
+ inflections.singular(/(n)ews$/i, '\1ews')
60
+ inflections.singular(/([ti])a$/i, '\1um')
61
+ inflections.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
62
+ inflections.singular(/(^analy)(sis|ses)$/i, '\1sis')
63
+ inflections.singular(/([^f])ves$/i, '\1fe')
64
+ inflections.singular(/(hive)s$/i, '\1')
65
+ inflections.singular(/(tive)s$/i, '\1')
66
+ inflections.singular(/([lr])ves$/i, '\1f')
67
+ inflections.singular(/([^aeiouy]|qu)ies$/i, '\1y')
68
+ inflections.singular(/(s)eries$/i, '\1eries')
69
+ inflections.singular(/(m)ovies$/i, '\1ovie')
70
+ inflections.singular(/(x|ch|ss|sh)es$/i, '\1')
71
+ inflections.singular(/^(m|l)ice$/i, '\1ouse')
72
+ inflections.singular(/(bus)(es)?$/i, '\1')
73
+ inflections.singular(/(o)es$/i, '\1')
74
+ inflections.singular(/(shoe)s$/i, '\1')
75
+ inflections.singular(/(cris|test)(is|es)$/i, '\1is')
76
+ inflections.singular(/^(a)x[ie]s$/i, '\1xis')
77
+ inflections.singular(/(octop|vir)(us|i)$/i, '\1us')
78
+ inflections.singular(/(alias|status)(es)?$/i, '\1')
79
+ inflections.singular(/^(ox)en/i, '\1')
80
+ inflections.singular(/(vert|ind)ices$/i, '\1ex')
81
+ inflections.singular(/(matr)ices$/i, '\1ix')
82
+ inflections.singular(/(quiz)zes$/i, '\1')
83
+ inflections.singular(/(database)s$/i, '\1')
84
+
85
+ # ActiveSupport adds a rule for both forms of an irregular, so that the
86
+ # plural pluralizes to itself ("people" => "people"). Every word here has
87
+ # the same first letter in both forms, which is all ActiveSupport's rules
88
+ # for this case need.
89
+ {
90
+ "person" => "people",
91
+ "man" => "men",
92
+ "child" => "children",
93
+ "sex" => "sexes",
94
+ "move" => "moves",
95
+ "zombie" => "zombies"
96
+ }.each do |singular, plural|
97
+ s0, srest = singular[0], singular[1..-1]
98
+ p0, prest = plural[0], plural[1..-1]
99
+
100
+ inflections.plural(/(#{s0})#{srest}$/i, "\\1#{prest}")
101
+ inflections.plural(/(#{p0})#{prest}$/i, "\\1#{prest}")
102
+
103
+ inflections.singular(/(#{s0})#{srest}$/i, "\\1#{srest}")
104
+ inflections.singular(/(#{p0})#{prest}$/i, "\\1#{srest}")
105
+ end
106
+
107
+ # dry-inflector considers a word uncountable if any of its
108
+ # underscore-separated segments is ("user_money"), and its list has words
109
+ # ActiveSupport's doesn't (moose, deer, ...). Drop it and use #uncountable?
110
+ # below instead.
111
+ inflections.uncountables.clear
112
+ end
113
+
17
114
  def pluralize(word)
18
- ActiveSupport::Inflector.pluralize(word.to_s)
115
+ word = word.to_s
116
+ uncountable?(word) ? word : INFLECTOR.pluralize(word)
19
117
  end
20
118
 
21
119
  def singularize(word)
22
- ActiveSupport::Inflector.singularize(word.to_s)
120
+ word = word.to_s
121
+ uncountable?(word) ? word : INFLECTOR.singularize(word)
23
122
  end
24
123
 
25
124
  #
@@ -27,25 +126,52 @@ class Class2
27
126
  # "user_statuses" => "UserStatus", "admin/user" => "Admin::User".
28
127
  #
29
128
  def classify(word)
30
- ActiveSupport::Inflector.classify(word.to_s)
129
+ camelize(singularize(word.to_s.sub(/.*\./, "")))
31
130
  end
32
131
 
33
132
  #
34
133
  # "some_value" => "SomeValue", "some_value", true => "someValue".
35
134
  # "/" becomes "::".
36
135
  #
37
- # ActiveSupport's second argument is the opposite of `lower`: it is true
38
- # when the first letter should be upper cased.
136
+ # Unlike ActiveSupport::Inflector#camelize this has no acronym support, so
137
+ # there's no need to pass a symbol to get "lower" camel case.
39
138
  #
40
139
  def camelize(word, lower = false)
41
- ActiveSupport::Inflector.camelize(word.to_s, !lower)
140
+ word = word.to_s.dup
141
+
142
+ if lower
143
+ word.sub!(/^\w/) { |c| c.downcase }
144
+ else
145
+ word.sub!(/^[a-z\d]*/) { |c| c.capitalize }
146
+ end
147
+
148
+ word.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
149
+ word.gsub!("/", "::")
150
+ word
42
151
  end
43
152
 
44
153
  #
45
- # "SomeValue" => "some_value", "Admin::User" => "admin/user".
154
+ # "SomeValue" => "some_value", "APIClient" => "api_client",
155
+ # "Admin::User" => "admin/user".
46
156
  #
47
157
  def underscore(word)
48
- ActiveSupport::Inflector.underscore(word.to_s)
158
+ word = word.to_s.dup
159
+ return word unless word =~ /[A-Z-]|::/
160
+
161
+ word.gsub!("::", "/")
162
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
163
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
164
+ word.tr!("-", "_")
165
+ word.downcase!
166
+ word
167
+ end
168
+
169
+ private
170
+
171
+ # ActiveSupport only considers the whole word, e.g. "police" is uncountable
172
+ # but "military_police" is not.
173
+ def uncountable?(word)
174
+ UNCOUNTABLE.any? { |uncountable| /\b#{Regexp.escape(uncountable)}\Z/i =~ word }
49
175
  end
50
176
  end
51
177
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Class2
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/class2.rb CHANGED
@@ -4,8 +4,6 @@ require "date"
4
4
  require "time" # for parse()
5
5
  require "json"
6
6
  require "rbconfig" # for autoload()
7
- require "active_support/core_ext/module"
8
- require "active_support/inflector"
9
7
 
10
8
  require "class2/version"
11
9
  require "class2/inflector"
@@ -145,7 +143,7 @@ class Class2
145
143
  object.each { |klass, attrs| make_class(namespace, klass, attrs, block) }
146
144
  end
147
145
 
148
- name = name.to_s.classify
146
+ name = Inflector.classify(name)
149
147
  return if namespace.const_defined?(name, false)
150
148
 
151
149
  make_method_name = lambda { |x| x.to_s.gsub(/[^\w]+/, "_") } # good enough
@@ -234,7 +232,7 @@ class Class2
234
232
  method = make_method_name[method]
235
233
  attr_writer method
236
234
 
237
- retval = method == method.pluralize ? "[]" : "#{namespace}::#{method.classify}.new"
235
+ retval = method == Inflector.pluralize(method) ? "[]" : "#{namespace}::#{Inflector.classify(method)}.new"
238
236
  class_eval <<-CODE
239
237
  def #{method}
240
238
  @#{method} ||= #{retval}
@@ -259,10 +257,11 @@ class Class2
259
257
  if self.class.__nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
260
258
  (value.is_a?(Hash) || value.is_a?(Array))
261
259
 
262
- name = key.to_s.classify
260
+ name = Inflector.classify(key)
263
261
 
264
- # parent is deprecated in ActiveSupport 6 and its warning uses Strong#squish! which they don't include!
265
- parent = self.class.respond_to?(:module_parent) ? self.class.module_parent : self.class.parent
262
+ # Neither Module#parent nor #module_parent, both ActiveSupport additions.
263
+ # For a class with no name this is Object, like #module_parent.
264
+ parent = self.class.name.to_s.split("::")[0..-2].inject(Object) { |mod, c| mod.const_get(c) }
266
265
  next unless parent.const_defined?(name)
267
266
 
268
267
  klass = parent.const_get(name)
@@ -303,7 +302,7 @@ class Class2
303
302
  module UpperCamelCase
304
303
  module Attributes
305
304
  def self.included(klass)
306
- Util.convert_attributes(klass) { |v| v.camelize }
305
+ Util.convert_attributes(klass) { |v| Inflector.camelize(v) }
307
306
  end
308
307
  end
309
308
 
@@ -324,13 +323,13 @@ class Class2
324
323
  module LowerCamelCase
325
324
  module Attributes
326
325
  def self.included(klass)
327
- Util.convert_attributes(klass) { |v| v.camelize(:lower) }
326
+ Util.convert_attributes(klass) { |v| Inflector.camelize(v, true) }
328
327
  end
329
328
  end
330
329
 
331
330
  module JSON
332
331
  def as_json(*)
333
- Util.as_json(self, :camelize, :lower)
332
+ Util.as_json(self, :camelize, true)
334
333
  end
335
334
 
336
335
  def to_json(*argz)
@@ -353,7 +352,7 @@ class Class2
353
352
  #
354
353
  module Attributes
355
354
  def self.included(klass)
356
- Util.convert_attributes(klass) { |v| v.underscore }
355
+ Util.convert_attributes(klass) { |v| Inflector.underscore(v) }
357
356
  end
358
357
  end
359
358
 
@@ -373,18 +372,18 @@ class Class2
373
372
  end
374
373
 
375
374
  module Util
376
- def self.as_json(klass, *argz)
375
+ def self.as_json(klass, method, *argz)
377
376
  hash = {}
378
377
  klass.to_h.each do |k, v|
379
378
  if v.is_a?(Hash)
380
- v = as_json(v, *argz)
379
+ v = as_json(v, method, *argz)
381
380
  elsif v.is_a?(Array)
382
- v = v.map { |e| as_json(e, *argz) }
381
+ v = v.map { |e| as_json(e, method, *argz) }
383
382
  elsif v.respond_to?(:as_json)
384
383
  v = v.as_json
385
384
  end
386
385
 
387
- hash[k.to_s.public_send(*argz)] = v
386
+ hash[Inflector.public_send(method, k.to_s, *argz)] = v
388
387
  end
389
388
 
390
389
  hash
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: class2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
@@ -11,25 +11,19 @@ cert_chain: []
11
11
  date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
14
+ name: dry-inflector
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '3.2'
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '9'
19
+ version: 1.1.0
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '3.2'
30
- - - "<"
24
+ - - "~>"
31
25
  - !ruby/object:Gem::Version
32
- version: '9'
26
+ version: 1.1.0
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: bundler
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,20 +52,6 @@ dependencies:
58
52
  - - "~>"
59
53
  - !ruby/object:Gem::Version
60
54
  version: '13'
61
- - !ruby/object:Gem::Dependency
62
- name: appraisal
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
55
  - !ruby/object:Gem::Dependency
76
56
  name: minitest
77
57
  requirement: !ruby/object:Gem::Requirement
@@ -95,16 +75,12 @@ extra_rdoc_files: []
95
75
  files:
96
76
  - ".github/workflows/ci.yml"
97
77
  - ".gitignore"
98
- - Appraisals
99
78
  - Changes
100
79
  - Gemfile
101
80
  - LICENSE.txt
102
81
  - README.md
103
82
  - Rakefile
104
83
  - class2.gemspec
105
- - gemfiles/as4.gemfile
106
- - gemfiles/as5.gemfile
107
- - gemfiles/as6.gemfile
108
84
  - lib/class2.rb
109
85
  - lib/class2/autoload.rb
110
86
  - lib/class2/autoload/namespaced.rb
@@ -122,7 +98,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
98
  requirements:
123
99
  - - ">="
124
100
  - !ruby/object:Gem::Version
125
- version: '0'
101
+ version: '2.7'
126
102
  required_rubygems_version: !ruby/object:Gem::Requirement
127
103
  requirements:
128
104
  - - ">="
data/Appraisals DELETED
@@ -1,11 +0,0 @@
1
- appraise "as4" do
2
- gem "activesupport", "~> 4.2"
3
- end
4
-
5
- appraise "as5" do
6
- gem "activesupport", "~> 5.2"
7
- end
8
-
9
- appraise "as6" do
10
- gem "activesupport", "> 5", "< 7"
11
- end
data/gemfiles/as4.gemfile DELETED
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "~> 4.2"
6
-
7
- gemspec path: "../"
data/gemfiles/as5.gemfile DELETED
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "~> 5.2"
6
-
7
- gemspec path: "../"
data/gemfiles/as6.gemfile DELETED
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "> 5", "< 7"
6
-
7
- gemspec path: "../"