adjustable_schema 0.10.0 → 0.11.1

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: 4525066ba26fd96f436d9e4620260becf28e8dade751b5caee3b56aa00791211
4
- data.tar.gz: 7f3d844b8be42f563aaf20fcf5b0b1519efc7a9b7e465fb2e06231a57be68326
3
+ metadata.gz: 2857a886692a24ac2506137f2962966e6bbcde416b8b3fed091e0fc219a8b81a
4
+ data.tar.gz: f46a4a33c5f3cb201bfdcebbd54c6e1854c3b1f21eb44ba1a05befea056c8d47
5
5
  SHA512:
6
- metadata.gz: 1bd658b2800d4d355057e5d17e032b1eee417dd5a74537cc24fb50dfd42a6785423a71c437e536ad2885ae5d0b78ce498aa9efe4f8367eace50b78f314d5cbd6
7
- data.tar.gz: 263cdad0cf81bfb3b1a050ef98c78d4738019732b262aa23a08a759e8ef07e25e9110af092ad6ba54e28ad7356be7b64da103d4438b28822ed077529bc8be0a3
6
+ metadata.gz: 44e84ec51c7d255d309499fad94e346062d7cb07164d202148f71c9e54ab8f00fd18d728053e504405fbc01860b1213a35d128dc81f1be2fa0ec6d357340b512
7
+ data.tar.gz: deb4ad146c70fd8ccd8eafbe943d49f68e8ce4526afc7e8ec44adad3e0cb937c3c1e82f394775b36405bb9312f4324f4760dc417e8c34a767ffaa11e5a166ea1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [0.11.1] — 2025-05-19
2
+
3
+ ### Fixed
4
+
5
+ - Associations setup for inherited models got broken in v0.11.0.
6
+
7
+
8
+ ## [0.11.0] — 2025-05-09
9
+
10
+ ### Changed
11
+
12
+ - Deprecated `Relationship.seed!` in favor of `AdjustableSchema.relationship!`.
13
+ - Special naming rules for “actor-like” models:
14
+ switched from `<associat>ful` form to check for related records’ presence back to a passive one (`<associat>ed`).
15
+ Thus, it’s `authored` and `edited` now instead of `authorful` and `editful`.
16
+ - `Relationship.to`/`.of` handle STI classes precisely instead of falling back to a base class.
17
+ - Customized inspections for `Relationship`.
18
+
19
+ ### Added
20
+
21
+ - `referencing`/`referenced_by` scopes to filter by related records.
22
+ - `#referencing!`/`#referenced_by!` setters to add related records.
23
+ - Short-cut methods for related records.
24
+ For example, `authors` association provides the following extras:
25
+ - `.authored_by` scope to filter records by authors,
26
+ - `#authored_by?` — to check if the record is authored by provided ones,
27
+ - `#authored_by!` — to add an author.
28
+
29
+
1
30
  ## [0.10.0] — 2025-04-23
2
31
 
3
32
  ### Changed
data/README.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Adjustable Schema for Rails
2
2
 
3
+ ![GitHub Actions Workflow Status](
4
+ https://img.shields.io/github/actions/workflow/status/Alexander-Senko/adjustable_schema/ci.yml
5
+ )
6
+ ![Code Climate maintainability](
7
+ https://img.shields.io/codeclimate/maintainability-percentage/Alexander-Senko/adjustable_schema
8
+ )
9
+ ![Code Climate coverage](
10
+ https://img.shields.io/codeclimate/coverage/Alexander-Senko/adjustable_schema
11
+ )
12
+
3
13
  Define your model associations in the database without changing the schema or models.
4
14
 
5
15
  This Rails Engine was renamed and refactored from [Rails Dynamic Associations](https://github.com/Alexander-Senko/rails_dynamic_associations).
@@ -24,7 +34,7 @@ AdjustableSchema::Relationship.create! source_type: 'Person',
24
34
  Or use a helper method:
25
35
 
26
36
  ``` ruby
27
- AdjustableSchema::Relationship.seed! Person => Book
37
+ AdjustableSchema.relationship! Person => Book
28
38
  ```
29
39
 
30
40
  Now you have:
@@ -39,7 +49,7 @@ book.people
39
49
  You can create multiple role-based associations between two models.
40
50
 
41
51
  ``` ruby
42
- AdjustableSchema::Relationship.seed! Person => Book, roles: %w[author editor]
52
+ AdjustableSchema.relationship! Person => Book, roles: %w[author editor]
43
53
  ```
44
54
 
45
55
  You will get:
@@ -61,7 +71,7 @@ book.editor_people
61
71
  In case you have set up relationships with `User` model you'll get a slightly different naming:
62
72
 
63
73
  ``` ruby
64
- AdjustableSchema::Relationship.seed! User => Book, roles: %w[author editor]
74
+ AdjustableSchema.relationship! User => Book, roles: %w[author editor]
65
75
  ```
66
76
 
67
77
  ``` ruby
@@ -87,7 +97,7 @@ end
87
97
  You may want to set up recursive relationships:
88
98
 
89
99
  ``` ruby
90
- AdjustableSchema::Relationship.seed! Person, roles: %w[friend]
100
+ AdjustableSchema.relationship! Person, roles: %w[friend]
91
101
  ```
92
102
 
93
103
  In this case you'll get these associations:
@@ -38,9 +38,9 @@ module AdjustableSchema
38
38
  case object
39
39
  when ::ActiveRecord::Base, nil
40
40
  where association => object
41
- when Class
42
- where "#{association}_type" => object.ancestors
43
- .grep(..object.base_class)
41
+ when ...::ActiveRecord::Base
42
+ where "#{association}_type" => object.descendants
43
+ .including(object)
44
44
  .map(&:name)
45
45
  when ::ActiveRecord::Relation
46
46
  send(method, object.klass)
@@ -99,27 +99,13 @@ module AdjustableSchema
99
99
  .reduce &:or
100
100
  end
101
101
 
102
- def seed! *models, roles: [], **mapping # rubocop:disable Metrics
103
- return seed!({ **Hash[*models], **mapping }, roles:) if mapping.any? # support keyword arguments syntax
104
-
105
- case models
106
- in [
107
- String | Symbol | Class => source_type,
108
- String | Symbol | Class => target_type,
109
- ]
110
- roles
111
- .map { |name| Role.find_or_create_by! name: }
112
- .then { it.presence or [ nil ] } # no roles => nameless relationship
113
- .map { |role| create! source_type:, target_type:, role: }
114
- in [ Hash => models ]
115
- for sources, targets in models do
116
- for source, target in Array(sources).product Array(targets) do
117
- seed! source, target, roles:
118
- end
119
- end
120
- in [ Class => source ]
121
- seed! source, source, roles: # recursive
122
- end
102
+ def seed!(...)
103
+ AdjustableSchema.deprecator.warn <<~TEXT.squish
104
+ #{self}.#{__method__} is deprecated and will be removed in v0.12.
105
+ Please use AdjustableSchema.relationship! instead.
106
+ TEXT
107
+
108
+ AdjustableSchema.relationship!(...)
123
109
  end
124
110
  end
125
111
 
@@ -131,5 +117,19 @@ module AdjustableSchema
131
117
  end
132
118
 
133
119
  def abstract? = not (source or target)
120
+
121
+ def inspect
122
+ <<~TEXT.chomp.gsub(/-+/, '-')
123
+ #<#{self.class}##{id}: #{
124
+ source_type
125
+ }#{
126
+ source_id&.then { "##{it}" }
127
+ }>-#{name}->#{
128
+ target_type unless target_type == source_type and target_id
129
+ }#{
130
+ target_id&.then { "##{it}" }
131
+ }>
132
+ TEXT
133
+ end
134
134
  end
135
135
  end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memery'
4
+
3
5
  module AdjustableSchema
4
6
  module ActiveRecord # :nodoc:
5
7
  concern :Associations do
6
8
  class_methods do
7
- private
9
+ include Memery
10
+
11
+ protected
12
+
13
+ memoize def adjust_associations
14
+ adjust_parent_associations
8
15
 
9
- def adjust_associations
10
16
  relationships
11
17
  .flat_map do |direction, relationships|
12
18
  relationships
@@ -19,6 +25,17 @@ module AdjustableSchema
19
25
  &.tap do # finally, if any relationships have been set up
20
26
  include Relationships::InstanceMethods
21
27
  end
28
+ &.size # optimization
29
+ end
30
+
31
+ private
32
+
33
+ def adjust_parent_associations
34
+ return unless superclass < Associations
35
+
36
+ superclass.adjust_associations
37
+
38
+ _reflections.reverse_merge! superclass._reflections # update
22
39
  end
23
40
 
24
41
  def setup_association direction, target = self, role = nil
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema
4
+ module ActiveRecord
5
+ module Relationships
6
+ module Builder # :nodoc:
7
+ private
8
+
9
+ def define_reference_scope direction, name
10
+ scope name, -> records, role: ANY do
11
+ joins(relationships = :"#{direction}_relationships")
12
+ .distinct
13
+ .where(
14
+ relationships => { id: Relationship
15
+ .send(Config.shortcuts[direction], records) # [to|of]: records
16
+ .then do
17
+ case role
18
+ when ANY
19
+ it
20
+ when nil
21
+ it.nameless
22
+ else
23
+ it.named *role
24
+ end
25
+ end,
26
+ },
27
+ )
28
+ end
29
+ end
30
+
31
+ def define_reference_setter direction, name
32
+ define_method "#{name}!" do |records, **options|
33
+ reference! direction => records, **options
34
+ end
35
+ end
36
+
37
+ def define_recursive_method association_name, method
38
+ define_method method do
39
+ send(association_name)
40
+ .recursive
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -5,35 +5,44 @@ require 'memery'
5
5
  module AdjustableSchema
6
6
  module ActiveRecord # :nodoc:
7
7
  concern :Relationships do
8
+ ANY = Object.new
9
+
8
10
  class_methods do
9
11
  include Memery
10
12
 
11
13
  memoize def relationships
12
- Config.association_directions.index_with do
13
- Relationship.abstract.send Config.shortcuts.opposite[it], self
14
- end
14
+ Config.association_directions
15
+ .with_opposite
16
+ .transform_values do
17
+ Relationship.abstract
18
+ .where "#{it}_type": name
19
+ end
15
20
  end
16
21
 
17
22
  def roles(&) = Role.of self, &
18
-
19
- private
20
-
21
- def define_recursive_methods association_name, method
22
- define_method method do
23
- send(association_name)
24
- .recursive
25
- end
26
- end
27
23
  end
28
24
 
29
25
  concern :InstanceMethods do # to include when needed
30
26
  included do
31
27
  scope :roleless, -> { merge Relationship.nameless }
32
28
 
29
+ extend Relationships::Builder
30
+
31
+ Config.association_directions.references
32
+ .select { reflect_on_association "#{_1}_relationships" }
33
+ .tap do
34
+ it
35
+ .reject { respond_to? _2 }
36
+ .each { define_reference_scope _1, _2 }
37
+ it
38
+ .reject { method_defined? "#{_2}!" }
39
+ .each { define_reference_setter _1, _2 }
40
+ end
41
+
33
42
  Config.association_directions.recursive
34
43
  .select { reflect_on_association _1 }
35
44
  .reject { method_defined? _2 }
36
- .each { define_recursive_methods _1, _2 }
45
+ .each { define_recursive_method _1, _2 }
37
46
  end
38
47
 
39
48
  def related?(...)
@@ -43,7 +52,6 @@ module AdjustableSchema
43
52
 
44
53
  def related(...)
45
54
  relationships(...)
46
- .preload(Config.association_directions)
47
55
  .map do |relationship|
48
56
  Config.association_directions
49
57
  .map { relationship.send it } # both objects
@@ -62,10 +70,26 @@ module AdjustableSchema
62
70
  .map { relationships_to it }
63
71
  .reduce(&:or)
64
72
  end
73
+ .preload(Config.association_directions)
65
74
  end
66
75
 
67
76
  private
68
77
 
78
+ def reference! source: self, target: self, role: nil
79
+ role &&= Relationship::Role[role]
80
+
81
+ [ source, target ]
82
+ .map { Array it }
83
+ .reduce(&:product)
84
+ .each do |source, target|
85
+ Relationship.create! source:, target:, role:
86
+ rescue ::ActiveRecord::RecordNotUnique
87
+ # That’s OK, it’s already there
88
+ end
89
+
90
+ self # for chainability
91
+ end
92
+
69
93
  def relationships_to direction
70
94
  try "#{direction}_relationships" or
71
95
  Relationship.none
@@ -44,6 +44,10 @@ module AdjustableSchema
44
44
  source: role.name,
45
45
  target: "#{role.name.passivize}_#{target_name}",
46
46
  }[direction]
47
+
48
+ def name_for_any(name = object_name) = name
49
+ .passivize
50
+ .to_sym
47
51
  end
48
52
 
49
53
  def initialize(...)
@@ -87,8 +91,6 @@ module AdjustableSchema
87
91
 
88
92
  def name_without_role = target_name
89
93
 
90
- def roleless_name = name(target_name)
91
-
92
94
  def name_for_any (name = object_name) = :"#{name}ful"
93
95
  def name_for_none(name = object_name) = :"#{name}less"
94
96
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema
4
+ module ActiveRecord
5
+ module Association::Referenced # :nodoc:
6
+ private
7
+
8
+ def scopes
9
+ role = self.role.name # save the context
10
+
11
+ {
12
+ **super,
13
+
14
+ referenced_name => -> { referenced_by it, role: },
15
+ }
16
+ end
17
+
18
+ def flags
19
+ name = self.name # save the context
20
+
21
+ {
22
+ **super,
23
+
24
+ referenced_name => -> { it.in? send name },
25
+ }
26
+ end
27
+
28
+ def setters
29
+ role = self.role.name # save the context
30
+
31
+ {
32
+ **super,
33
+
34
+ referenced_name => -> { referenced_by! it, role: },
35
+ }
36
+ end
37
+
38
+ using Association::Inflections
39
+
40
+ def referenced_name = :"#{object_name.passivize}_by"
41
+ end
42
+ end
43
+ end
@@ -22,6 +22,8 @@ module AdjustableSchema
22
22
  .merge Relationship.named *roles
23
23
  end
24
24
  end
25
+
26
+ def roleless_name = name target_name
25
27
  end
26
28
  end
27
29
  end
@@ -15,14 +15,22 @@ module AdjustableSchema
15
15
  require_relative 'association/scopes'
16
16
  require_relative 'association/roleless'
17
17
  require_relative 'association/hierarchy'
18
+ require_relative 'association/referenced'
18
19
 
19
20
  include Memery
20
21
 
22
+ class << self
23
+ include Memery
24
+
25
+ memoize :new # unique
26
+ end
27
+
21
28
  def initialize(...)
22
29
  super
23
30
 
24
- extend Roleless if roleless?
25
- extend Hierarchy if hierarchy?
31
+ extend Roleless if roleless?
32
+ extend Hierarchy if hierarchy?
33
+ extend Referenced if source? and role and target.actor?
26
34
  end
27
35
 
28
36
  def define
@@ -31,7 +39,7 @@ module AdjustableSchema
31
39
  has_many name, **options do
32
40
  include Scopes
33
41
  include Scopes::Recursive if association.recursive?
34
- end
42
+ end or return
35
43
 
36
44
  define_scopes
37
45
  define_methods
@@ -58,13 +66,15 @@ module AdjustableSchema
58
66
 
59
67
  def define_scopes
60
68
  scopes
61
- .reject { owner.singleton_class.method_defined? _1 }
69
+ .reject { owner.respond_to? _1 }
62
70
  .each { owner.scope _1, _2 }
63
71
  end
64
72
 
65
73
  def define_methods
66
- flags
67
- .transform_keys { :"#{it}?" }
74
+ {
75
+ **flag_methods,
76
+ **setter_methods,
77
+ }
68
78
  .reject { owner.method_defined? _1 }
69
79
  .each { owner.define_method _1, &_2 }
70
80
  end
@@ -93,6 +103,14 @@ module AdjustableSchema
93
103
  }
94
104
  end
95
105
 
106
+ def flag_methods = flags
107
+ .transform_keys { :"#{it}?" }
108
+
109
+ def setters = {}
110
+
111
+ def setter_methods = setters
112
+ .transform_keys { :"#{it}!" }
113
+
96
114
  memoize def options = {
97
115
  through: define_relationships,
98
116
  source: direction,
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema
4
+ module ActiveRecord
5
+ module Builder # :nodoc:
6
+ def relationship! *models, roles: [], **mapping
7
+ return relationship!({ **Hash[*models], **mapping }, roles:) if
8
+ mapping.any? # support keyword arguments syntax
9
+
10
+ case models
11
+ in [
12
+ String | Symbol | Class => source,
13
+ String | Symbol | Class => target,
14
+ ]
15
+ define_relationship source, target,
16
+ roles:
17
+ in [ Hash => models ]
18
+ define_relationships models,
19
+ roles:
20
+ in [ String | Symbol | Class => model ]
21
+ define_recursive_relationship model,
22
+ roles:
23
+ end
24
+ end
25
+
26
+ def role! name
27
+ Relationship::Role.find_or_create_by! name:
28
+ end
29
+
30
+ private
31
+
32
+ def define_recursive_relationship model, roles: []
33
+ define_relationship model, model, roles:
34
+ end
35
+
36
+ def define_relationships models, roles: []
37
+ models
38
+ .flat_map { it.map { Array it }.reduce &:product }
39
+ .each { define_relationship *it, roles: }
40
+ end
41
+
42
+ def define_relationship source_type, target_type, roles: []
43
+ roles
44
+ .map { role! it }
45
+ .then { it.presence or [ nil ] } # no roles => nameless relationship
46
+ .map { |role| Relationship.create! source_type:, target_type:, role: }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -3,6 +3,7 @@
3
3
  module AdjustableSchema
4
4
  module ActiveRecord # :nodoc:
5
5
  autoload :Association, 'adjustable_schema/active_record/association'
6
+ autoload :Builder, 'adjustable_schema/active_record/builder'
6
7
 
7
8
  ActiveSupport.on_load :active_record do
8
9
  include Associations
@@ -21,7 +21,8 @@ module AdjustableSchema
21
21
  end
22
22
  end
23
23
 
24
- def self = config :self
24
+ def self = config :self
25
+ def references = config :reference
25
26
 
26
27
  def recursive
27
28
  config.values.to_h do
@@ -33,6 +34,10 @@ module AdjustableSchema
33
34
  grep_v(to).sole
34
35
  end
35
36
 
37
+ def with_opposite
38
+ index_with { opposite to: it }
39
+ end
40
+
36
41
  private
37
42
 
38
43
  def config section = nil
@@ -11,11 +11,13 @@ module AdjustableSchema
11
11
  source: {
12
12
  shortcut: :of,
13
13
  self: :child,
14
+ reference: :referenced_by,
14
15
  recursive: :descendants,
15
16
  },
16
17
  target: {
17
18
  shortcut: :to,
18
19
  self: :parent,
20
+ reference: :referencing,
19
21
  recursive: :ancestors,
20
22
  },
21
23
  },
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AdjustableSchema
4
- VERSION = '0.10.0'
4
+ VERSION = '0.11.1'
5
5
  end
@@ -9,9 +9,15 @@ require 'rails_model_load_hook' # should be loaded
9
9
  module AdjustableSchema # :nodoc:
10
10
  autoload :Config, 'adjustable_schema/config'
11
11
 
12
+ extend ActiveRecord::Builder
13
+
12
14
  module_function
13
15
 
14
16
  def available?
15
17
  Relationship.table_exists?
16
18
  end
19
+
20
+ def deprecator # :nodoc:
21
+ @deprecator ||= ActiveSupport::Deprecation.new
22
+ end
17
23
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adjustable_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-23 00:00:00.000000000 Z
10
+ date: 2025-05-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -82,6 +82,7 @@ files:
82
82
  - app/models/adjustable_schema/relationship/role.rb
83
83
  - app/models/concerns/adjustable_schema/active_record/associations.rb
84
84
  - app/models/concerns/adjustable_schema/active_record/relationships.rb
85
+ - app/models/concerns/adjustable_schema/active_record/relationships/builder.rb
85
86
  - app/models/concerns/adjustable_schema/actors.rb
86
87
  - config/initializers/associations.rb
87
88
  - config/initializers/model_names.rb
@@ -91,8 +92,10 @@ files:
91
92
  - lib/adjustable_schema/active_record/association.rb
92
93
  - lib/adjustable_schema/active_record/association/hierarchy.rb
93
94
  - lib/adjustable_schema/active_record/association/naming.rb
95
+ - lib/adjustable_schema/active_record/association/referenced.rb
94
96
  - lib/adjustable_schema/active_record/association/roleless.rb
95
97
  - lib/adjustable_schema/active_record/association/scopes.rb
98
+ - lib/adjustable_schema/active_record/builder.rb
96
99
  - lib/adjustable_schema/authors.rb
97
100
  - lib/adjustable_schema/config.rb
98
101
  - lib/adjustable_schema/engine.rb
@@ -104,7 +107,7 @@ licenses:
104
107
  metadata:
105
108
  homepage_uri: https://github.com/Alexander-Senko/adjustable_schema
106
109
  source_code_uri: https://github.com/Alexander-Senko/adjustable_schema
107
- changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.10.0/CHANGELOG.md
110
+ changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.11.1/CHANGELOG.md
108
111
  rdoc_options: []
109
112
  require_paths:
110
113
  - lib