adjustable_schema 0.8.0 → 0.9.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Rakefile +7 -5
  4. data/app/models/adjustable_schema/application_record.rb +5 -3
  5. data/app/models/adjustable_schema/relationship/role.rb +8 -1
  6. data/app/models/adjustable_schema/relationship.rb +37 -19
  7. data/app/models/concerns/adjustable_schema/active_record/associations.rb +3 -1
  8. data/app/models/concerns/adjustable_schema/active_record/relationships.rb +9 -7
  9. data/config/initializers/associations.rb +2 -0
  10. data/config/initializers/model_names.rb +2 -0
  11. data/db/migrate/01_create_adjustable_schema_relationship_tables.rb +3 -1
  12. data/lib/adjustable_schema/active_record/association/hierarchy.rb +33 -0
  13. data/lib/adjustable_schema/active_record/association/naming.rb +31 -26
  14. data/lib/adjustable_schema/active_record/association/roleless.rb +27 -0
  15. data/lib/adjustable_schema/active_record/association/scopes.rb +30 -27
  16. data/lib/adjustable_schema/active_record/association.rb +58 -61
  17. data/lib/adjustable_schema/active_record.rb +3 -1
  18. data/lib/adjustable_schema/authors.rb +3 -1
  19. data/lib/adjustable_schema/config.rb +8 -9
  20. data/lib/adjustable_schema/engine.rb +3 -1
  21. data/lib/adjustable_schema/version.rb +3 -1
  22. data/lib/adjustable_schema.rb +5 -3
  23. data/lib/tasks/adjustable_schema_tasks.rake +2 -0
  24. metadata +9 -16
  25. data/app/assets/config/adjustable_schema_manifest.js +0 -1
  26. data/app/assets/stylesheets/adjustable_schema/application.css +0 -15
  27. data/app/controllers/adjustable_schema/application_controller.rb +0 -4
  28. data/app/helpers/adjustable_schema/application_helper.rb +0 -4
  29. data/app/jobs/adjustable_schema/application_job.rb +0 -4
  30. data/app/mailers/adjustable_schema/application_mailer.rb +0 -6
  31. data/app/views/layouts/adjustable_schema/application.html.erb +0 -15
  32. data/config/routes.rb +0 -2
  33. data/lib/adjustable_schema/active_record/query_methods.rb +0 -103
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e36fef6757e9e0803ad940f90a425ea0b25870c0720b0d338ed14970a35e339
4
- data.tar.gz: a50db598919a729c33f607f107b1f78145d3fe262e00d0b7899dacb473ebc194
3
+ metadata.gz: 3b1390e4f564f54d0e8a840fae882bf7a494b51323846ab06e05a786c332fc37
4
+ data.tar.gz: 5458b1a98b4b3a3dea0c3a8e2cd66e42003683cbbd4af95c939af8fe1810ce34
5
5
  SHA512:
6
- metadata.gz: 631771aa7ea4295ba2d886ab24a1e102b1ab50b9d3631ff467a3f46fd6f48a5bcd1f4ff2af62523510cc4c5d361852ef71dd183ca5846d20cf3a939ba25864f8
7
- data.tar.gz: 57cd60293cb11377429fb2f808115301bb84f3a9c71372c7e0e5040bc8441e3ca62b244906d96785f1a34845c9542a1617107d7887dd01b9481b2b658ecae1d5
6
+ metadata.gz: dee15ab2468fca2b6bf3739df1b65508dd80ae87830833d27895e05a21c59392e3856ffbd78742cc0c067bfd1db66b364a880f0bf5401a3a3cc5cac2879ec59e
7
+ data.tar.gz: 984449dfcba1f1797830b5153439d2873187b0fe32aaefe23cb21bab76b49f49906f70dc562eacb92653a531eb828c262449bf41a71b4507782de25ac38214d0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.9.0] — 2025-03-10
2
+
3
+ ### Changed
4
+
5
+ - Requires Rails 8 and Ruby 3.4+.
6
+ - Protected `AdjustableSchema::Relationship::Role` from being deleted when used.
7
+
8
+ ### Added
9
+
10
+ - `Relationship#sourced` and `Relationship#targeted` scopes to filter relationships by presence of _source_ and _target_ records respectively.
11
+
12
+ ### Fixed
13
+
14
+ - `Relationship#applied` scope used to return relationships with at least one record attached, instead of ones with both records present.
15
+
16
+
1
17
  ## [0.8.0] — 2024-11-08
2
18
 
3
19
  ### Changed
data/Rakefile CHANGED
@@ -1,8 +1,10 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
- load "rails/tasks/engine.rake"
3
+ require 'bundler/setup'
5
4
 
6
- load "rails/tasks/statistics.rake"
5
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
7
 
8
- require "bundler/gem_tasks"
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
- class ApplicationRecord < ::ActiveRecord::Base
3
- self.abstract_class = true
4
- end
4
+ class ApplicationRecord < ::ActiveRecord::Base # :nodoc:
5
+ self.abstract_class = true
6
+ end
5
7
  end
@@ -1,9 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
4
  class Relationship
5
+ # = Relationship roles
6
+ #
7
+ # `AdjustableSchema::Relationship::Role` serves to distinguish
8
+ # between several associations of the same pair of models.
3
9
  class Role < ApplicationRecord
4
10
  include Organizer::Identifiable.by :name, symbolized: true
5
11
 
6
- has_many :relationships
12
+ has_many :relationships,
13
+ dependent: :restrict_with_exception
7
14
 
8
15
  validates :name, presence: true, uniqueness: true
9
16
 
@@ -1,4 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
4
+ # = Relationships
5
+ #
6
+ # `Relationship` is a core class of Adjustable Schema representing both
7
+ # associations of model classes and connections of individual records.
8
+ #
9
+ # No constraints are supported yet, so only many-to-many associations
10
+ # are available for now.
11
+ #
12
+ # == Model associations
13
+ #
14
+ # To represent an association, a `Relationship` record should have both
15
+ # `source_type` and `target_type` columns set, with both ID columns set
16
+ # to `NULL`.
17
+ #
18
+ # == Record relationships
19
+ #
20
+ # To connect individual records, both `source` and `target` polymorphic
21
+ # associations shoud be set.
22
+ #
23
+ # == Roles
24
+ #
25
+ # Many associations with different semantics between the same models
26
+ # can be set using roles (see `Relationship::Role`).
2
27
  class Relationship < ApplicationRecord
3
28
  belongs_to :source, polymorphic: true, optional: true
4
29
  belongs_to :target, polymorphic: true, optional: true
@@ -8,7 +33,7 @@ module AdjustableSchema
8
33
  includes :role
9
34
  end
10
35
 
11
- Config.shortcuts.each &-> ((association, method)) do
36
+ Config.shortcuts.each &-> ((association, method)) do # rubocop:disable Style
12
37
  scope method, -> object {
13
38
  case object
14
39
  when ::ActiveRecord::Base, nil
@@ -29,8 +54,7 @@ module AdjustableSchema
29
54
 
30
55
  scope "#{method}_abstract", -> object = nil {
31
56
  if object
32
- send(__method__).
33
- send method, object
57
+ send(__method__).send method, object
34
58
  else
35
59
  where "#{association}_id" => nil
36
60
  end
@@ -39,20 +63,14 @@ module AdjustableSchema
39
63
 
40
64
  scope :abstract, -> {
41
65
  Config.shortcuts.values
42
- .map { send _1, :abstract }
66
+ .map { send it, :abstract }
43
67
  .reduce &:merge
44
68
  }
45
69
 
46
- scope :general, -> {
47
- where target: nil
48
- }
49
-
50
- scope :applied, -> {
51
- where.not(
52
- source: nil,
53
- target: nil,
54
- )
55
- }
70
+ scope :general, -> { where target: nil }
71
+ scope :sourced, -> { where.not source: nil }
72
+ scope :targeted, -> { where.not target: nil }
73
+ scope :applied, -> { sourced.targeted }
56
74
 
57
75
  scope :named, -> *names {
58
76
  case names
@@ -74,15 +92,15 @@ module AdjustableSchema
74
92
  def [] **scopes
75
93
  scopes
76
94
  .map do
77
- self
95
+ self # rubocop:disable Style
78
96
  .send(Config.shortcuts[:source], _1)
79
97
  .send(Config.shortcuts[:target], _2)
80
98
  end
81
99
  .reduce &:or
82
100
  end
83
101
 
84
- def seed! *models, roles: [], **_models
85
- return seed!({ **Hash[*models], **_models }, roles:) if _models.any? # support keyword arguments syntax
102
+ def seed! *models, roles: [], **mapping # rubocop:disable Metrics
103
+ return seed!({ **Hash[*models], **mapping }, roles:) if mapping.any? # support keyword arguments syntax
86
104
 
87
105
  case models
88
106
  in [
@@ -91,7 +109,7 @@ module AdjustableSchema
91
109
  ]
92
110
  roles
93
111
  .map { |name| Role.find_or_create_by! name: }
94
- .then { _1.presence or [ nil ] } # no roles => nameless relationship
112
+ .then { it.presence or [ nil ] } # no roles => nameless relationship
95
113
  .map { |role| create! source_type:, target_type:, role: }
96
114
  in [ Hash => models ]
97
115
  for sources, targets in models do
@@ -122,7 +140,7 @@ module AdjustableSchema
122
140
  # https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations
123
141
  reflections
124
142
  .values
125
- .select { _1.options[:polymorphic] }
143
+ .select { it.options[:polymorphic] }
126
144
  .each do |reflection|
127
145
  define_method "#{reflection.name}_type=" do |type|
128
146
  super type && type.to_s.classify.constantize.base_class.to_s
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
- module ActiveRecord
4
+ module ActiveRecord # :nodoc:
3
5
  concern :Associations do
4
6
  class_methods do
5
7
  private
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memery'
2
4
 
3
5
  module AdjustableSchema
4
- module ActiveRecord
6
+ module ActiveRecord # :nodoc:
5
7
  concern :Relationships do
6
8
  class_methods do
7
9
  include Memery
8
10
 
9
11
  memoize def relationships
10
- Config.association_directions.to_h do
11
- [ _1, Relationship.abstract.send(Config.shortcuts.opposite[_1], self) ]
12
+ Config.association_directions.index_with do
13
+ Relationship.abstract.send Config.shortcuts.opposite[it], self
12
14
  end
13
15
  end
14
16
 
@@ -44,20 +46,20 @@ module AdjustableSchema
44
46
  .preload(Config.association_directions)
45
47
  .map do |relationship|
46
48
  Config.association_directions
47
- .map { relationship.send _1 } # both objects
49
+ .map { relationship.send it } # both objects
48
50
  .without(self) # the related one
49
51
  .first or self # may be recursive
50
52
  end
51
53
  .uniq
52
54
  end
53
55
 
54
- def relationships **options
55
- if (direction, scope = Config.find_direction **options) # filter by direction & related objects
56
+ def relationships(...)
57
+ if (direction, scope = Config.find_direction(...)) # filter by direction & related objects
56
58
  relationships_to(direction)
57
59
  .send Config.shortcuts[direction], scope
58
60
  else # all in both directions
59
61
  Config.association_directions
60
- .map { relationships_to _1 }
62
+ .map { relationships_to it }
61
63
  .reduce(&:or)
62
64
  end
63
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActiveSupport.on_load :model_class do
2
4
  next if self == AdjustableSchema::Relationship
3
5
  next unless AdjustableSchema.available?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # HACK: non-public Rails API used
2
4
  ActiveModel::Name.class_eval do
3
5
  def unnamespaced = @unnamespaced || name
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreateAdjustableSchemaRelationshipTables < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  # Use Active Record's configured type for primary and foreign keys
@@ -22,7 +24,7 @@ class CreateAdjustableSchemaRelationshipTables < ActiveRecord::Migration[7.1]
22
24
  target_id target_type
23
25
  role_id
24
26
  ].tap { |columns|
25
- columns.reject! { _1.ends_with? '_type' } if foreign_key_type == :uuid # OPTIMIZATION: IDs are unique
27
+ columns.reject! { it.ends_with? '_type' } if foreign_key_type == :uuid # OPTIMIZATION: IDs are unique
26
28
 
27
29
  # NULLS are DISTINCT by default.
28
30
  # One can use `ADD CONSTRAINT … UNIQUE NULLS NOT DISTINCT (…)` instead
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema
4
+ module ActiveRecord
5
+ module Association::Hierarchy # :nodoc:
6
+ private
7
+
8
+ def scopes
9
+ name = roleless_name # save the context
10
+
11
+ {
12
+ **super,
13
+
14
+ name_for_any( target_name) => -> { where.associated name },
15
+ name_for_none(target_name) => -> { where.missing name },
16
+ }
17
+ end
18
+
19
+ def flags
20
+ name = roleless_name # save the context
21
+
22
+ {
23
+ **super,
24
+
25
+ name_for_any( target_name) => -> { send(name).any? },
26
+ name_for_none(target_name) => -> { send(name).none? },
27
+ intermediate: -> { send(name).one? },
28
+ branching: -> { send(name).many? },
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memery'
2
4
 
3
5
  module AdjustableSchema
4
6
  module ActiveRecord
5
- class Association
7
+ class Association # :nodoc:
6
8
  concerning :Naming do
7
9
  include Memery
8
10
 
9
- module Inflections
11
+ module Inflections # :nodoc:
10
12
  refine String do
11
13
  def passivize
12
- self
13
- .presence
14
+ presence
14
15
  &.sub(/((:?[aeiou]+[^aeiou]+){2,})(?:or|ant|ion|e?ment)$/, '\1ed')
15
16
  &.sub(/((:?[aeiou]+[^aeiou]+){1,})(?:ing)$/, '\1ed')
16
17
  &.sub(/(?:e*|ed|er)$/, '\1ed')
@@ -21,6 +22,27 @@ module AdjustableSchema
21
22
 
22
23
  using Inflections
23
24
 
25
+ module Recursive # :nodoc:
26
+ include Memery
27
+
28
+ memoize def name_with_role = {
29
+ source: role.name,
30
+ target: "#{role.name.passivize}_#{target_name}",
31
+ }[direction]
32
+
33
+ def name_without_role
34
+ Config.association_directions
35
+ .self[direction]
36
+ .to_s
37
+ end
38
+ end
39
+
40
+ def initialize(...)
41
+ super
42
+
43
+ extend Recursive if recursive?
44
+ end
45
+
24
46
  memoize def name name = object_name
25
47
  name
26
48
  .to_s
@@ -48,29 +70,12 @@ module AdjustableSchema
48
70
 
49
71
  private
50
72
 
51
- memoize def name_with_role
52
- if recursive?
53
- {
54
- source: role.name,
55
- target: "#{role.name.passivize}_#{target_name}",
56
- }[direction]
57
- else
58
- "#{{
59
- source: role.name,
60
- target: role.name.passivize,
61
- }[direction]}_#{target_name}"
62
- end
63
- end
73
+ memoize def name_with_role = "#{{
74
+ source: role.name,
75
+ target: role.name.passivize,
76
+ }[direction]}_#{target_name}"
64
77
 
65
- def name_without_role
66
- if recursive?
67
- Config.association_directions
68
- .self[direction]
69
- .to_s
70
- else
71
- target_name
72
- end
73
- end
78
+ def name_without_role = target_name
74
79
 
75
80
  def roleless_name = name(target_name)
76
81
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdjustableSchema
4
+ module ActiveRecord
5
+ module Association::Roleless # :nodoc:
6
+ def define
7
+ super
8
+
9
+ has_many roleless_name, -> { roleless }, **options if
10
+ child?
11
+ end
12
+
13
+ private
14
+
15
+ def define_methods
16
+ super
17
+
18
+ name = self.name # save the context
19
+
20
+ owner.redefine_method "#{name}_with_roles" do |*roles|
21
+ send(name)
22
+ .merge Relationship.named *roles
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,39 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
4
  module ActiveRecord
3
5
  module Association::Scopes
4
- concern :Recursive do
5
- require_relative '../query_methods'
6
-
7
- included do
8
- ::ActiveRecord::QueryMethods.prepend QueryMethods # HACK: to bring `with.recursive` in
9
- end
6
+ module Recursive # :nodoc:
7
+ # rubocop:disable Layout
10
8
 
11
9
  def recursive
12
- all._exec_scope do
13
- all
14
- .select(
15
- select_values = self.select_values.presence || arel_table[Arel.star],
16
- Arel.sql('1').as('distance'),
17
- )
18
- .with.recursive(recursive_table.name => unscoped
19
- .select(
20
- select_values,
21
- (recursive_table[:distance] + 1).as('distance'),
22
- )
23
- .joins(inverse_association_name)
24
- .arel
25
- .join(recursive_table)
26
- .on(recursive_table[primary_key].eq inverse_table[primary_key])
27
- )
28
- .unscope(:select, :joins, :where)
29
- .from(recursive_table.alias table_name)
30
- .distinct
31
- .unscope(:order) # for SELECT DISTINCT, ORDER BY expressions must appear in select list
32
- end
10
+ with_recursive(recursive_table.name => [ recursion_base, recursive_step ])
11
+ .unscope(:select, :joins, :where)
12
+ .from(recursive_table.alias table_name)
13
+ .distinct
14
+ .unscope(:order) # for SELECT DISTINCT, ORDER BY expressions must appear in select list
33
15
  end
34
16
 
35
17
  private
36
18
 
19
+ def recursion_base
20
+ unscope(:order, :group, :having)
21
+ .select(recursive_select_values,
22
+ Arel.sql('1').as('distance'),
23
+ )
24
+ end
25
+
26
+ def recursive_step
27
+ unscoped
28
+ .select(recursive_select_values,
29
+ (recursive_table[:distance] + 1).as('distance'),
30
+ )
31
+ .joins(inverse_association_name)
32
+ .joins(<<~SQL.squish)
33
+ JOIN #{recursive_table.name}
34
+ ON #{recursive_table.name}.#{primary_key} = #{inverse_table.name}.#{primary_key}
35
+ SQL
36
+ end
37
+
37
38
  def association_name = @association.reflection.name
38
39
 
39
40
  def inverse_association_name
@@ -46,6 +47,8 @@ module AdjustableSchema
46
47
 
47
48
  def recursive_table = Arel::Table.new [ :recursive, association_name, klass.table_name ] * '_'
48
49
  def inverse_table = Arel::Table.new [ inverse_association_name, klass.table_name ] * '_' # HACK: depends on ActiveRecord internals
50
+
51
+ def recursive_select_values = select_values.presence || arel_table[Arel.star]
49
52
  end
50
53
  end
51
54
  end
@@ -1,35 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'memery'
4
+
1
5
  module AdjustableSchema
2
6
  module ActiveRecord
3
- class Association < Struct.new(:owner, :direction, :target, :role)
7
+ # rubocop:disable Style/StructInheritance
8
+ class Association < Struct.new( # :nodoc:
9
+ :owner,
10
+ :direction,
11
+ :target,
12
+ :role,
13
+ )
4
14
  require_relative 'association/naming'
5
15
  require_relative 'association/scopes'
16
+ require_relative 'association/roleless'
17
+ require_relative 'association/hierarchy'
18
+
19
+ include Memery
20
+
21
+ def initialize(...)
22
+ super
23
+
24
+ extend Roleless if roleless?
25
+ extend Hierarchy if hierarchy?
26
+ end
6
27
 
7
28
  def define
8
- name.tap do |association_name|
9
- association = self # save context
10
-
11
- has_many association_name, **(options = {
12
- through: define_relationships,
13
- source: direction,
14
- source_type: target.base_class.name,
15
- class_name: target.name
16
- }) do
17
- include Scopes
18
- include Scopes::Recursive if association.recursive?
19
- end
20
-
21
- define_scopes
22
- define_methods
23
-
24
- unless role
25
- # HACK: using `try` to overcome a Rails bug
26
- # (see https://github.com/rails/rails/issues/40109)
27
- has_many roleless_name, -> { try :roleless }, **options if
28
- child?
29
-
30
- define_role_methods
31
- end
29
+ association = self # save the context
30
+
31
+ has_many name, **options do
32
+ include Scopes
33
+ include Scopes::Recursive if association.recursive?
32
34
  end
35
+
36
+ define_scopes
37
+ define_methods
33
38
  end
34
39
 
35
40
  def recursive? = target == owner
@@ -52,56 +57,48 @@ module AdjustableSchema
52
57
  end
53
58
 
54
59
  def define_scopes
55
- name = relationships_name
56
- roleless_name = self.roleless_name
57
-
58
- {
59
- name_for_any => -> { where.associated name },
60
- name_for_none => -> { where.missing name },
61
-
62
- **({
63
- name_for_any( target_name) => -> { where.associated roleless_name },
64
- name_for_none(target_name) => -> { where.missing roleless_name },
65
- } if hierarchy?),
66
- }
60
+ scopes
67
61
  .reject { owner.singleton_class.method_defined? _1 }
68
62
  .each { owner.scope _1, _2 }
69
63
  end
70
64
 
71
65
  def define_methods
72
- name = self.name
73
- roleless_name = self.roleless_name
74
-
75
- {
76
- name_for_any => -> { send(name).any? },
77
- name_for_none => -> { send(name).none? },
78
-
79
- **(hierarchy? ? {
80
- name_for_any( target_name) => -> { send(roleless_name).any? },
81
- name_for_none(target_name) => -> { send(roleless_name).none? },
82
- intermediate: -> { send(roleless_name).one? },
83
- branching: -> { send(roleless_name).many? },
84
- } : {}),
85
- }
86
- .transform_keys {"#{_1}?" }
66
+ flags
67
+ .transform_keys { :"#{it}?" }
87
68
  .reject { owner.method_defined? _1 }
88
69
  .each { owner.define_method _1, &_2 }
89
70
  end
90
71
 
91
- def define_role_methods
92
- name = self.name
93
-
94
- owner.redefine_method "#{name}_with_roles" do |*roles|
95
- send(name)
96
- .merge Relationship.named *roles
97
- end
98
- end
99
-
100
72
  def has_many(association_name, ...)
101
73
  return if owner.reflect_on_association association_name
102
74
 
103
75
  owner.has_many(association_name, ...)
104
76
  end
77
+
78
+ def scopes
79
+ name = relationships_name # save the context
80
+
81
+ {
82
+ name_for_any => -> { where.associated name },
83
+ name_for_none => -> { where.missing name },
84
+ }
85
+ end
86
+
87
+ def flags
88
+ name = self.name # save the context
89
+
90
+ {
91
+ name_for_any => -> { send(name).any? },
92
+ name_for_none => -> { send(name).none? },
93
+ }
94
+ end
95
+
96
+ memoize def options = {
97
+ through: define_relationships,
98
+ source: direction,
99
+ source_type: target.base_class.name,
100
+ class_name: target.name,
101
+ }
105
102
  end
106
103
  end
107
104
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
- module ActiveRecord
4
+ module ActiveRecord # :nodoc:
3
5
  autoload :Association, 'adjustable_schema/active_record/association'
4
6
 
5
7
  ActiveSupport.on_load :active_record do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Gem::Author ||= Struct.new(
2
4
  :name,
3
5
  :email,
@@ -11,5 +13,5 @@ module AdjustableSchema
11
13
  email: 'Alexander.Senko@gmail.com',
12
14
  github_url: 'https://github.com/Alexander-Senko',
13
15
  ),
14
- ]
16
+ ].freeze
15
17
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'memery'
2
4
 
3
5
  module AdjustableSchema
4
- module Config
6
+ module Config # :nodoc:
5
7
  include Memery
6
8
 
7
- module Naming
9
+ module Naming # :nodoc:
8
10
  include Memery
9
11
 
10
12
  memoize def shortcuts
@@ -13,7 +15,7 @@ module AdjustableSchema
13
15
  if to
14
16
  values.grep_v(to).sole
15
17
  else
16
- transform_values { opposite to: _1 }
18
+ transform_values { opposite to: it }
17
19
  end
18
20
  end
19
21
  end
@@ -23,7 +25,7 @@ module AdjustableSchema
23
25
 
24
26
  def recursive
25
27
  config.values.to_h do
26
- [ _1[:self].to_s.pluralize.to_sym, _1[:recursive].to_sym ]
28
+ [ it[:self].to_s.pluralize.to_sym, it[:recursive].to_sym ]
27
29
  end
28
30
  end
29
31
 
@@ -35,7 +37,7 @@ module AdjustableSchema
35
37
 
36
38
  def config section = nil
37
39
  if section
38
- config.transform_values { _1[section].to_sym }
40
+ config.transform_values { it[section].to_sym }
39
41
  else
40
42
  Config.association_names # TODO: DRY
41
43
  end
@@ -64,14 +66,11 @@ module AdjustableSchema
64
66
  module_function method
65
67
  end
66
68
 
67
- private
68
- module_function
69
-
70
69
  def association_names = Engine.config.names[:associations]
71
70
 
72
71
  def normalize **options
73
72
  shortcuts
74
- .tap { options.assert_valid_keys _1.keys, _1.values }
73
+ .tap { options.assert_valid_keys it.keys, it.values }
75
74
  .select { _2.in? options }
76
75
  .each { options[_1] = options.delete _2 }
77
76
 
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'organizer'
2
4
 
3
5
  module AdjustableSchema
4
- class Engine < ::Rails::Engine
6
+ class Engine < ::Rails::Engine # :nodoc:
5
7
  isolate_namespace AdjustableSchema
6
8
 
7
9
  config.names = {
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdjustableSchema
2
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
3
5
  end
@@ -1,10 +1,12 @@
1
- require "adjustable_schema/version"
2
- require "adjustable_schema/engine"
1
+ # frozen_string_literal: true
2
+
3
+ require 'adjustable_schema/version'
4
+ require 'adjustable_schema/engine'
3
5
  require 'adjustable_schema/active_record'
4
6
 
5
7
  require 'rails_model_load_hook' # should be loaded
6
8
 
7
- module AdjustableSchema
9
+ module AdjustableSchema # :nodoc:
8
10
  autoload :Config, 'adjustable_schema/config'
9
11
 
10
12
  module_function
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # desc "Explaining what the task does"
2
4
  # task :adjustable_schema do
3
5
  # # Task goes here
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-11-08 00:00:00.000000000 Z
10
+ date: 2025-03-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '8.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '7.1'
25
+ version: '8.0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rails_model_load_hook
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -77,28 +77,21 @@ files:
77
77
  - MIT-LICENSE
78
78
  - README.md
79
79
  - Rakefile
80
- - app/assets/config/adjustable_schema_manifest.js
81
- - app/assets/stylesheets/adjustable_schema/application.css
82
- - app/controllers/adjustable_schema/application_controller.rb
83
- - app/helpers/adjustable_schema/application_helper.rb
84
- - app/jobs/adjustable_schema/application_job.rb
85
- - app/mailers/adjustable_schema/application_mailer.rb
86
80
  - app/models/adjustable_schema/application_record.rb
87
81
  - app/models/adjustable_schema/relationship.rb
88
82
  - app/models/adjustable_schema/relationship/role.rb
89
83
  - app/models/concerns/adjustable_schema/active_record/associations.rb
90
84
  - app/models/concerns/adjustable_schema/active_record/relationships.rb
91
- - app/views/layouts/adjustable_schema/application.html.erb
92
85
  - config/initializers/associations.rb
93
86
  - config/initializers/model_names.rb
94
- - config/routes.rb
95
87
  - db/migrate/01_create_adjustable_schema_relationship_tables.rb
96
88
  - lib/adjustable_schema.rb
97
89
  - lib/adjustable_schema/active_record.rb
98
90
  - lib/adjustable_schema/active_record/association.rb
91
+ - lib/adjustable_schema/active_record/association/hierarchy.rb
99
92
  - lib/adjustable_schema/active_record/association/naming.rb
93
+ - lib/adjustable_schema/active_record/association/roleless.rb
100
94
  - lib/adjustable_schema/active_record/association/scopes.rb
101
- - lib/adjustable_schema/active_record/query_methods.rb
102
95
  - lib/adjustable_schema/authors.rb
103
96
  - lib/adjustable_schema/config.rb
104
97
  - lib/adjustable_schema/engine.rb
@@ -110,7 +103,7 @@ licenses:
110
103
  metadata:
111
104
  homepage_uri: https://github.com/Alexander-Senko/adjustable_schema
112
105
  source_code_uri: https://github.com/Alexander-Senko/adjustable_schema
113
- changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.8.0/CHANGELOG.md
106
+ changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.9.0/CHANGELOG.md
114
107
  rdoc_options: []
115
108
  require_paths:
116
109
  - lib
@@ -118,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
111
  requirements:
119
112
  - - ">="
120
113
  - !ruby/object:Gem::Version
121
- version: '3.2'
114
+ version: '3.4'
122
115
  required_rubygems_version: !ruby/object:Gem::Requirement
123
116
  requirements:
124
117
  - - ">="
125
118
  - !ruby/object:Gem::Version
126
119
  version: '0'
127
120
  requirements: []
128
- rubygems_version: 3.6.0.dev
121
+ rubygems_version: 3.6.5
129
122
  specification_version: 4
130
123
  summary: Adjustable data schemas for Rails
131
124
  test_files: []
@@ -1 +0,0 @@
1
- //= link_directory ../stylesheets/adjustable_schema .css
@@ -1,15 +0,0 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
@@ -1,4 +0,0 @@
1
- module AdjustableSchema
2
- class ApplicationController < ActionController::Base
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module AdjustableSchema
2
- module ApplicationHelper
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module AdjustableSchema
2
- class ApplicationJob < ActiveJob::Base
3
- end
4
- end
@@ -1,6 +0,0 @@
1
- module AdjustableSchema
2
- class ApplicationMailer < ActionMailer::Base
3
- default from: "from@example.com"
4
- layout "mailer"
5
- end
6
- end
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Adjustable schema</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
7
-
8
- <%= stylesheet_link_tag "adjustable_schema/application", media: "all" %>
9
- </head>
10
- <body>
11
-
12
- <%= yield %>
13
-
14
- </body>
15
- </html>
data/config/routes.rb DELETED
@@ -1,2 +0,0 @@
1
- AdjustableSchema::Engine.routes.draw do
2
- end
@@ -1,103 +0,0 @@
1
- module AdjustableSchema
2
- module ActiveRecord
3
- module QueryMethods
4
- class WithChain
5
- def initialize scope
6
- @scope = scope
7
- end
8
-
9
- # Returns a new relation expressing WITH RECURSIVE statement.
10
- #
11
- # #recursive accepts conditions as a hash. See QueryMethods#with for
12
- # more details on each format.
13
- #
14
- # User.with.recursive(
15
- # descendants: User.joins('INNER JOIN descendants ON users.parent_id = descendants.id')
16
- # )
17
- # # WITH RECURSIVE descendants AS (
18
- # # SELECT * FROM users
19
- # # UNION
20
- # # SELECT * FROM users INNER JOIN descendants ON users.parent_id = descendants.id
21
- # # ) SELECT * FROM descendants
22
- #
23
- # WARNING! Due to how Arel works,
24
- # * `recursive` can't be chained with any prior non-recursive calls to `with` and
25
- # * all subsequent non-recursive calls to `with` will be treated as recursive ones.
26
- def recursive *args
27
- args.map! do
28
- next _1 unless _1.is_a? Hash
29
-
30
- _1
31
- .map(&method(:with_recursive_union))
32
- .to_h
33
- end
34
-
35
- case @scope.with_values
36
- in []
37
- @scope = @scope.with :recursive, *args
38
- in [ :recursive, * ]
39
- @scope = @scope.with *args
40
- else
41
- raise ArgumentError, "can't chain `WITH RECURSIVE` with non-recursive one"
42
- end
43
-
44
- @scope
45
- end
46
-
47
- private
48
-
49
- def with_recursive_union name, scope
50
- scope = scope.arel if scope.respond_to? :arel
51
-
52
- [
53
- name,
54
- @scope
55
- .unscope(:order, :group, :having)
56
- .arel
57
- .union(scope)
58
- ]
59
- end
60
- end
61
-
62
- def with *args
63
- if args.empty?
64
- WithChain.new spawn
65
- else
66
- super
67
- end
68
- end
69
-
70
- private
71
-
72
- # OVERWRITE: allow a Symbol to be passes as the first argument
73
- def build_with(arel)
74
- return if with_values.empty?
75
-
76
- with_statements = with_values.map.with_index do |with_value, i|
77
- next with_value if with_value.is_a? Symbol and i == 0
78
-
79
- raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}" unless with_value.is_a?(Hash)
80
-
81
- build_with_value_from_hash(with_value)
82
- end
83
-
84
- arel.with(*with_statements)
85
- end
86
-
87
- # OVERWRITE: allow Arel Nodes
88
- def build_with_value_from_hash(hash)
89
- hash.map do |name, value|
90
- expression =
91
- case value
92
- when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
93
- when ::ActiveRecord::Relation then value.arel
94
- when Arel::SelectManager, Arel::Nodes::Node then value
95
- else
96
- raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
97
- end
98
- Arel::Nodes::TableAlias.new(expression, name)
99
- end
100
- end
101
- end
102
- end
103
- end