adjustable_schema 0.9.0 → 0.11.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: 3b1390e4f564f54d0e8a840fae882bf7a494b51323846ab06e05a786c332fc37
4
- data.tar.gz: 5458b1a98b4b3a3dea0c3a8e2cd66e42003683cbbd4af95c939af8fe1810ce34
3
+ metadata.gz: 5514bed45ea4246eb3af8f2331cbc0d911aa3582fadd0cf6fb3ec6fbb61f9c00
4
+ data.tar.gz: e042af98603878baa75b1f2a922157dd764572f568d3a836b73c509e654dd2d3
5
5
  SHA512:
6
- metadata.gz: dee15ab2468fca2b6bf3739df1b65508dd80ae87830833d27895e05a21c59392e3856ffbd78742cc0c067bfd1db66b364a880f0bf5401a3a3cc5cac2879ec59e
7
- data.tar.gz: 984449dfcba1f1797830b5153439d2873187b0fe32aaefe23cb21bab76b49f49906f70dc562eacb92653a531eb828c262449bf41a71b4507782de25ac38214d0
6
+ metadata.gz: e5d3a6f67b13c183c2728582df56f03337362b087e8486130758cdaacd87c72215dd74099a98be202d185b7a84c044d892469122f6850fadc81de6c3e98036a4
7
+ data.tar.gz: 2e7113dd0df58bb104086316d4c883e3fe96c2e1eb3b96bb201567937a799edd16502c303fd05a091a83d8afb6e3481a4157a7d2402fc596a80abb01fe493866
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## [0.11.0] — 2025-05-09
2
+
3
+ ### Changed
4
+
5
+ - Deprecated `Relationship.seed!` in favor of `AdjustableSchema.relationship!`.
6
+ - Special naming rules for “actor-like” models:
7
+ switched from `<associat>ful` form to check for related records’ presence back to a passive one (`<associat>ed`).
8
+ Thus, it’s `authored` and `edited` now instead of `authorful` and `editful`.
9
+ - `Relationship.to`/`.of` handle STI classes precisely instead of falling back to a base class.
10
+ - Customized inspections for `Relationship`.
11
+
12
+ ### Added
13
+
14
+ - `referencing`/`referenced_by` scopes to filter by related records.
15
+ - `#referencing!`/`#referenced_by!` setters to add related records.
16
+ - Short-cut methods for related records.
17
+ For example, `authors` association provides the following extras:
18
+ - `.authored_by` scope to filter records by authors,
19
+ - `#authored_by?` — to check if the record is authored by provided ones,
20
+ - `#authored_by!` — to add an author.
21
+
22
+
23
+ ## [0.10.0] — 2025-04-23
24
+
25
+ ### Changed
26
+
27
+ - No more polymorphic STI associations tricks (see 0dd30220 for details).
28
+
29
+ ### Added
30
+
31
+ - Special naming rules for “actor-like” models.
32
+
33
+
1
34
  ## [0.9.0] — 2025-03-10
2
35
 
3
36
  ### Changed
data/README.md CHANGED
@@ -24,7 +24,7 @@ AdjustableSchema::Relationship.create! source_type: 'Person',
24
24
  Or use a helper method:
25
25
 
26
26
  ``` ruby
27
- AdjustableSchema::Relationship.seed! Person => Book
27
+ AdjustableSchema.relationship! Person => Book
28
28
  ```
29
29
 
30
30
  Now you have:
@@ -39,7 +39,7 @@ book.people
39
39
  You can create multiple role-based associations between two models.
40
40
 
41
41
  ``` ruby
42
- AdjustableSchema::Relationship.seed! Person => Book, roles: %w[author editor]
42
+ AdjustableSchema.relationship! Person => Book, roles: %w[author editor]
43
43
  ```
44
44
 
45
45
  You will get:
@@ -56,12 +56,12 @@ book.editor_people
56
56
 
57
57
  #### Special cases
58
58
 
59
- ##### "Actor-like" models
59
+ ##### Actor-like models
60
60
 
61
61
  In case you have set up relationships with `User` model you'll get a slightly different naming:
62
62
 
63
63
  ``` ruby
64
- AdjustableSchema::Relationship.seed! User => Book, roles: %w[author editor]
64
+ AdjustableSchema.relationship! User => Book, roles: %w[author editor]
65
65
  ```
66
66
 
67
67
  ``` ruby
@@ -73,12 +73,21 @@ book.editors
73
73
  The list of models to be handled this way can be set with `actor_model_names` configuration parameter.
74
74
  It includes `User` by default.
75
75
 
76
+ ``` ruby
77
+ AdjustableSchema::Engine.configure do
78
+ config.actor_model_names << 'Person'
79
+ end
80
+ ```
81
+
82
+ > [!CAUTION]
83
+ > Names are passed instead of model classes not to mess the loading up.
84
+
76
85
  ##### Self-referencing models
77
86
 
78
87
  You may want to set up recursive relationships:
79
88
 
80
89
  ``` ruby
81
- AdjustableSchema::Relationship.seed! Person, roles: %w[friend]
90
+ AdjustableSchema.relationship! Person, roles: %w[friend]
82
91
  ```
83
92
 
84
93
  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
 
@@ -132,19 +118,18 @@ module AdjustableSchema
132
118
 
133
119
  def abstract? = not (source or target)
134
120
 
135
- # HACK
136
- # Using polymorphic associations in combination with single table inheritance (STI) is
137
- # a little tricky. In order for the associations to work as expected, ensure that you
138
- # store the base model for the STI models in the type column of the polymorphic
139
- # association.
140
- # https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations
141
- reflections
142
- .values
143
- .select { it.options[:polymorphic] }
144
- .each do |reflection|
145
- define_method "#{reflection.name}_type=" do |type|
146
- super type && type.to_s.classify.constantize.base_class.to_s
147
- end
148
- end
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
149
134
  end
150
135
  end
@@ -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,6 +5,8 @@ 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
 
@@ -15,25 +17,29 @@ module AdjustableSchema
15
17
  end
16
18
 
17
19
  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
20
  end
28
21
 
29
22
  concern :InstanceMethods do # to include when needed
30
23
  included do
31
24
  scope :roleless, -> { merge Relationship.nameless }
32
25
 
26
+ extend Relationships::Builder
27
+
28
+ Config.association_directions.references
29
+ .select { reflect_on_association "#{_1}_relationships" }
30
+ .tap do
31
+ it
32
+ .reject { respond_to? _2 }
33
+ .each { define_reference_scope _1, _2 }
34
+ it
35
+ .reject { method_defined? "#{_2}!" }
36
+ .each { define_reference_setter _1, _2 }
37
+ end
38
+
33
39
  Config.association_directions.recursive
34
40
  .select { reflect_on_association _1 }
35
41
  .reject { method_defined? _2 }
36
- .each { define_recursive_methods _1, _2 }
42
+ .each { define_recursive_method _1, _2 }
37
43
  end
38
44
 
39
45
  def related?(...)
@@ -43,7 +49,6 @@ module AdjustableSchema
43
49
 
44
50
  def related(...)
45
51
  relationships(...)
46
- .preload(Config.association_directions)
47
52
  .map do |relationship|
48
53
  Config.association_directions
49
54
  .map { relationship.send it } # both objects
@@ -62,10 +67,26 @@ module AdjustableSchema
62
67
  .map { relationships_to it }
63
68
  .reduce(&:or)
64
69
  end
70
+ .preload(Config.association_directions)
65
71
  end
66
72
 
67
73
  private
68
74
 
75
+ def reference! source: self, target: self, role: nil
76
+ role &&= Relationship::Role[role]
77
+
78
+ [ source, target ]
79
+ .map { Array it }
80
+ .reduce(&:product)
81
+ .each do |source, target|
82
+ Relationship.create! source:, target:, role:
83
+ rescue ::ActiveRecord::RecordNotUnique
84
+ # That’s OK, it’s already there
85
+ end
86
+
87
+ self # for chainability
88
+ end
89
+
69
90
  def relationships_to direction
70
91
  try "#{direction}_relationships" or
71
92
  Relationship.none
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema # :nodoc:
4
+ concern :Actors do
5
+ class_methods do
6
+ def actor?
7
+ Config.actor_models.any? { self <= it }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -37,10 +37,24 @@ module AdjustableSchema
37
37
  end
38
38
  end
39
39
 
40
+ module Actors # :nodoc:
41
+ include Memery
42
+
43
+ memoize def name_with_role = {
44
+ source: role.name,
45
+ target: "#{role.name.passivize}_#{target_name}",
46
+ }[direction]
47
+
48
+ def name_for_any(name = object_name) = name
49
+ .passivize
50
+ .to_sym
51
+ end
52
+
40
53
  def initialize(...)
41
54
  super
42
55
 
43
56
  extend Recursive if recursive?
57
+ extend Actors if target.actor?
44
58
  end
45
59
 
46
60
  memoize def name name = object_name
@@ -77,8 +91,6 @@ module AdjustableSchema
77
91
 
78
92
  def name_without_role = target_name
79
93
 
80
- def roleless_name = name(target_name)
81
-
82
94
  def name_for_any (name = object_name) = :"#{name}ful"
83
95
  def name_for_none(name = object_name) = :"#{name}less"
84
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,16 @@ 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
 
21
22
  def initialize(...)
22
23
  super
23
24
 
24
- extend Roleless if roleless?
25
- extend Hierarchy if hierarchy?
25
+ extend Roleless if roleless?
26
+ extend Hierarchy if hierarchy?
27
+ extend Referenced if source? and role and target.actor?
26
28
  end
27
29
 
28
30
  def define
@@ -58,13 +60,15 @@ module AdjustableSchema
58
60
 
59
61
  def define_scopes
60
62
  scopes
61
- .reject { owner.singleton_class.method_defined? _1 }
63
+ .reject { owner.respond_to? _1 }
62
64
  .each { owner.scope _1, _2 }
63
65
  end
64
66
 
65
67
  def define_methods
66
- flags
67
- .transform_keys { :"#{it}?" }
68
+ {
69
+ **flag_methods,
70
+ **setter_methods,
71
+ }
68
72
  .reject { owner.method_defined? _1 }
69
73
  .each { owner.define_method _1, &_2 }
70
74
  end
@@ -93,6 +97,14 @@ module AdjustableSchema
93
97
  }
94
98
  end
95
99
 
100
+ def flag_methods = flags
101
+ .transform_keys { :"#{it}?" }
102
+
103
+ def setters = {}
104
+
105
+ def setter_methods = setters
106
+ .transform_keys { :"#{it}!" }
107
+
96
108
  memoize def options = {
97
109
  through: define_relationships,
98
110
  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,10 +3,12 @@
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
9
10
  include Relationships
11
+ include Actors
10
12
  end
11
13
  end
12
14
  end
@@ -3,15 +3,23 @@
3
3
  Gem::Author ||= Struct.new(
4
4
  :name,
5
5
  :email,
6
- :github_url,
7
- )
6
+ :github,
7
+ ) do
8
+ def github_url = github && "https://github.com/#{github}"
9
+ end
8
10
 
9
- module AdjustableSchema
10
- AUTHORS = [
11
+ module AdjustableSchema # :nodoc:
12
+ AUTHORS = [ # rubocop:disable Style/MutableConstant
11
13
  Gem::Author.new(
12
- name: 'Alexander Senko',
13
- email: 'Alexander.Senko@gmail.com',
14
- github_url: 'https://github.com/Alexander-Senko',
14
+ name: 'Alexander Senko',
15
+ email: 'Alexander.Senko@gmail.com',
16
+ github: 'Alexander-Senko',
15
17
  ),
16
- ].freeze
18
+ ]
19
+
20
+ class << AUTHORS
21
+ def names = filter_map &:name
22
+ def emails = filter_map &:email
23
+ def github_url = filter_map(&:github_url).first
24
+ end
17
25
  end
@@ -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
@@ -66,6 +67,11 @@ module AdjustableSchema
66
67
  module_function method
67
68
  end
68
69
 
70
+ memoize def actor_models
71
+ Engine.config.actor_model_names
72
+ .filter_map &:safe_constantize
73
+ end
74
+
69
75
  def association_names = Engine.config.names[:associations]
70
76
 
71
77
  def normalize **options
@@ -11,14 +11,20 @@ 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
  },
22
24
  }
25
+
26
+ config.actor_model_names = %w[
27
+ User
28
+ ]
23
29
  end
24
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AdjustableSchema
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
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.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-09 00:00:00.000000000 Z
10
+ date: 2025-05-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -82,6 +82,8 @@ 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
86
+ - app/models/concerns/adjustable_schema/actors.rb
85
87
  - config/initializers/associations.rb
86
88
  - config/initializers/model_names.rb
87
89
  - db/migrate/01_create_adjustable_schema_relationship_tables.rb
@@ -90,8 +92,10 @@ files:
90
92
  - lib/adjustable_schema/active_record/association.rb
91
93
  - lib/adjustable_schema/active_record/association/hierarchy.rb
92
94
  - lib/adjustable_schema/active_record/association/naming.rb
95
+ - lib/adjustable_schema/active_record/association/referenced.rb
93
96
  - lib/adjustable_schema/active_record/association/roleless.rb
94
97
  - lib/adjustable_schema/active_record/association/scopes.rb
98
+ - lib/adjustable_schema/active_record/builder.rb
95
99
  - lib/adjustable_schema/authors.rb
96
100
  - lib/adjustable_schema/config.rb
97
101
  - lib/adjustable_schema/engine.rb
@@ -103,7 +107,7 @@ licenses:
103
107
  metadata:
104
108
  homepage_uri: https://github.com/Alexander-Senko/adjustable_schema
105
109
  source_code_uri: https://github.com/Alexander-Senko/adjustable_schema
106
- changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.9.0/CHANGELOG.md
110
+ changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.11.0/CHANGELOG.md
107
111
  rdoc_options: []
108
112
  require_paths:
109
113
  - lib