jpie 2.1.5 → 3.0.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: e03bce6fffc1ed66774e89f660afda17460b6d837b07f549b8c17698d252dccd
4
- data.tar.gz: 7a8ea2397e9ebfab913bfd7a4e13cab637b4e7fcc7829e7e16e37da019c5fd3e
3
+ metadata.gz: 23bcdb19123ced67be175eeeaace99cdfed44f3ce2dcd07134ec9742cf9c1af2
4
+ data.tar.gz: 38d365327b7991a82512e33dac60dc2649d0bef1ac03fcd5d999519015e72dda
5
5
  SHA512:
6
- metadata.gz: 27dc32299a441b790c9ae4f3890ad5f12f492d5a2027f1f4a0929923a79cbbaf1b1d78746e29d033c2ee85e4d9bf5c1dae24051d2f447eff5bea50a2a36c28b9
7
- data.tar.gz: 7fc2d6613e25d73ec7abaafc7dfe5066c9a7a4b79ea0ffc326964f114a3203909382ee68a62ee507617302309ad7517eb01f92ef4e28f2facd208e9cda769dad
6
+ metadata.gz: baf7cdcb438c6991aee5ca8b6181f5c4ff7ee84fc920be7777679f4ae00c22c06471860aaa40b3eac34affd82d2c509b8cd481866b37c69dbbca595155bfe2e2
7
+ data.tar.gz: 832ca3fb1eba8783e4bf006cb5e891650ffe711c19144dd7397aef2fd874eee7e5882e7a981d4e43e17ff666353ccf731b04ae0d1ea6945a52d2418d2d224ac9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jpie (2.1.4)
4
+ jpie (2.1.5)
5
5
  actionpack (~> 8.1, >= 8.1.0)
6
6
  pg_query (>= 4)
7
7
  prosopite (>= 1)
@@ -7,9 +7,14 @@ module JSONAPI
7
7
 
8
8
  rescue_from JSONAPI::AuthorizationError, with: :render_jsonapi_authorization_error
9
9
  rescue_from Pundit::NotAuthorizedError, with: :render_jsonapi_authorization_error if defined?(Pundit)
10
+ rescue_from JSONAPI::Support::Sort::InvalidFieldError, with: :render_invalid_sort_field_error
10
11
 
11
12
  private
12
13
 
14
+ def render_invalid_sort_field_error(error)
15
+ render_sort_errors(error.invalid_fields)
16
+ end
17
+
13
18
  def render_jsonapi_authorization_error(error)
14
19
  detail = error&.message.presence || "You are not authorized to perform this action"
15
20
  render json: {
@@ -110,15 +110,13 @@ module JSONAPI
110
110
  params[:page].permit(:number, :size).to_h
111
111
  end
112
112
 
113
- def invalid_sort_fields_for_columns(sorts, available_columns)
114
- sorts.filter_map do |sort_field|
115
- field = JSONAPI::RelationshipHelpers.extract_sort_field_name(sort_field)
116
- field unless available_columns.include?(field.to_s)
117
- end
118
- end
119
-
120
- def valid_sort_fields_for_resource(resource_class)
121
- resource_class.permitted_sortable_fields.map(&:to_s)
113
+ def render_sort_errors(invalid)
114
+ render_parameter_errors(
115
+ invalid,
116
+ title: "Invalid Sort Field",
117
+ detail_proc: ->(f) { "Invalid sort field requested: #{f}" },
118
+ source_proc: ->(_) { { parameter: "sort" } },
119
+ )
122
120
  end
123
121
  end
124
122
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json_api/support/sort_value_comparison"
3
+ require "json_api/support/sort"
4
4
 
5
5
  module JSONAPI
6
6
  module Relationships
@@ -13,99 +13,8 @@ module JSONAPI
13
13
  sorts = parse_sort_param
14
14
  return related if sorts.empty?
15
15
 
16
- related_model = association.klass
17
- db_sorts, virtual_sorts = partition_sorts_by_type(sorts, related_model)
18
-
19
- related = apply_db_sorts(related, db_sorts)
20
- apply_virtual_sorts_if_needed(related, virtual_sorts, related_model)
21
- end
22
-
23
- def partition_sorts_by_type(sorts, related_model)
24
- sorts.partition do |sort_field|
25
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
26
- related_model.column_names.include?(field.to_s)
27
- end
28
- end
29
-
30
- def apply_db_sorts(related, db_sorts)
31
- db_sorts.each do |sort_field|
32
- direction = RelationshipHelpers.extract_sort_direction(sort_field)
33
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
34
- related = related.order(field => direction)
35
- end
36
- related
37
- end
38
-
39
- def apply_virtual_sorts_if_needed(related, virtual_sorts, related_model)
40
- return related unless virtual_sorts.any?
41
-
42
- resource_class = ResourceLoader.find_for_model(related_model)
43
- sort_records_by_virtual_attributes(related.to_a, virtual_sorts, resource_class)
44
- end
45
-
46
- def sort_records_by_virtual_attributes(records, virtual_sorts, resource_class)
47
- records.sort do |a, b|
48
- compare_records(a, b, virtual_sorts, resource_class)
49
- end
50
- end
51
-
52
- def compare_records(record_a, record_b, virtual_sorts, resource_class)
53
- virtual_sorts.each do |sort_field|
54
- comparison = compare_by_field(record_a, record_b, sort_field, resource_class)
55
- return comparison unless comparison.zero?
56
- end
57
- 0
58
- end
59
-
60
- def compare_by_field(record_a, record_b, sort_field, resource_class)
61
- direction = RelationshipHelpers.extract_sort_direction(sort_field)
62
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
63
-
64
- value_a = get_virtual_value(record_a, field, resource_class)
65
- value_b = get_virtual_value(record_b, field, resource_class)
66
-
67
- comparison = compare_values(value_a, value_b)
68
- direction == :desc ? -comparison : comparison
69
- end
70
-
71
- def get_virtual_value(record, field, resource_class)
72
- resource_instance = resource_class.new(record, {})
73
- field_sym = field.to_sym
74
- return resource_instance.public_send(field_sym) if resource_instance.respond_to?(field_sym, false)
75
-
76
- nil
77
- end
78
-
79
- def compare_values(value_a, value_b)
80
- JSONAPI::Support::SortValueComparison.compare(value_a, value_b)
81
- end
82
-
83
- def validate_sort_param
84
- sorts = parse_sort_param
85
- return if sorts.empty?
86
-
87
- association = @resource.class.reflect_on_association(@relationship_name)
88
- return unless association&.collection?
89
-
90
- validate_sort_fields_for_association(sorts, association)
91
- end
92
-
93
- def validate_sort_fields_for_association(sorts, association)
94
16
  resource_class = ResourceLoader.find_for_model(association.klass)
95
- valid_fields = valid_sort_fields_for_resource(resource_class)
96
- invalid_fields = invalid_sort_fields_for_columns(sorts, valid_fields)
97
- return if invalid_fields.empty?
98
-
99
- render_invalid_sort_fields(invalid_fields)
100
- end
101
-
102
- def render_invalid_sort_fields(invalid_fields)
103
- render_parameter_errors(
104
- invalid_fields,
105
- title: "Invalid Sort Field",
106
- detail_proc: ->(field) { "Invalid sort field requested: #{field}" },
107
- source_proc: ->(_field) { { parameter: "sort" } },
108
- )
17
+ Support::Sort.new(sort_params: sorts, resource_class:).apply(related)
109
18
  end
110
19
  end
111
20
  end
@@ -13,15 +13,6 @@ module JSONAPI
13
13
  render_field_error(error) if error
14
14
  end
15
15
 
16
- def validate_sort_param
17
- sorts = parse_sort_param
18
- return if sorts.empty?
19
-
20
- valid = valid_sort_fields_for_resource(@resource_class)
21
- invalid = invalid_sort_fields_for_columns(sorts, valid)
22
- render_sort_errors(invalid) if invalid.any?
23
- end
24
-
25
16
  private
26
17
 
27
18
  def first_invalid_field(fields)
@@ -61,15 +52,6 @@ module JSONAPI
61
52
  source_proc: ->(f) { { parameter: "fields[#{error[:type]}]=#{f}" } },
62
53
  )
63
54
  end
64
-
65
- def render_sort_errors(invalid)
66
- render_parameter_errors(
67
- invalid,
68
- title: "Invalid Sort Field",
69
- detail_proc: ->(f) { "Invalid sort field requested: #{f}" },
70
- source_proc: ->(_) { { parameter: "sort" } },
71
- )
72
- end
73
55
  end
74
56
  end
75
57
  end
@@ -29,7 +29,6 @@ module JSONAPI
29
29
  before_action :load_jsonapi_resource
30
30
  before_action :validate_fields_param, only: %i[index show]
31
31
  before_action :validate_filter_param, only: [:index]
32
- before_action :validate_sort_param, only: [:index]
33
32
  before_action :validate_include_param, only: %i[index show]
34
33
  before_action :validate_resource_type!, only: %i[create update]
35
34
  before_action :validate_resource_id!, only: [:update]
@@ -25,8 +25,6 @@ module JSONAPI
25
25
  before_action :set_resource
26
26
  before_action :set_relationship_name
27
27
  before_action :validate_relationship_exists
28
- before_action :validate_sort_param, only: [:show]
29
-
30
28
  def show
31
29
  authorize_resource_action!(@resource, action: :show, context: { relationship: @relationship_name })
32
30
  render json: build_show_response, status: :ok
@@ -14,22 +14,15 @@ module JSONAPI
14
14
 
15
15
  def permitted_sortable_fields
16
16
  sort_fields = @sortable_fields || []
17
- sort_fields = inherited_sort_only_fields + sort_fields if should_inherit_sortable_fields?
18
- (permitted_attributes + sort_fields).uniq
17
+ sort_fields = superclass.permitted_sortable_fields + sort_fields if should_inherit_sortable_fields?
18
+ sort_fields.uniq
19
19
  end
20
20
 
21
21
  def should_inherit_sortable_fields?
22
22
  !instance_variable_defined?(:@sortable_fields) &&
23
- !instance_variable_defined?(:@attributes) &&
24
23
  superclass != JSONAPI::Resource &&
25
24
  superclass.respond_to?(:permitted_sortable_fields)
26
25
  end
27
-
28
- def inherited_sort_only_fields
29
- parent_sort_fields = superclass.permitted_sortable_fields
30
- parent_attributes = superclass.permitted_attributes
31
- parent_sort_fields - parent_attributes
32
- end
33
26
  end
34
27
  end
35
28
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "concerns/sorting"
3
+ require_relative "sort"
4
4
  require_relative "concerns/regular_filters"
5
5
  require_relative "concerns/nested_filters"
6
6
  require_relative "concerns/polymorphic_filters"
@@ -9,7 +9,6 @@ require_relative "concerns/pagination"
9
9
 
10
10
  module JSONAPI
11
11
  class CollectionQuery
12
- include Support::Sorting
13
12
  include Support::RegularFilters
14
13
  include Support::NestedFilters
15
14
  include Support::PolymorphicFilters
@@ -30,8 +29,9 @@ module JSONAPI
30
29
 
31
30
  def execute
32
31
  @scope = apply_filtering
32
+ @sort = Support::Sort.new(sort_params:, resource_class: definition)
33
33
  compute_total_count_if_needed
34
- @scope = apply_sorting(@scope)
34
+ @scope = @sort.apply(@scope)
35
35
  @scope = apply_pagination
36
36
  self
37
37
  end
@@ -43,8 +43,7 @@ module JSONAPI
43
43
  def compute_total_count_if_needed
44
44
  return unless pagination_applied
45
45
 
46
- has_virtual_sort = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
47
- @total_count = @scope.count if !has_virtual_sort || @scope.is_a?(Array)
46
+ @total_count = @scope.count if !@sort.virtual_fields? || @scope.is_a?(Array)
48
47
  end
49
48
 
50
49
  def apply_filtering
@@ -17,15 +17,6 @@ module JSONAPI
17
17
  TypeConversion.resource_type_name(definition_class)
18
18
  end
19
19
 
20
- # Delegate to SortParsing
21
- def extract_sort_field_name(sort_field)
22
- SortParsing.extract_sort_field_name(sort_field)
23
- end
24
-
25
- def extract_sort_direction(sort_field)
26
- SortParsing.extract_sort_direction(sort_field)
27
- end
28
-
29
20
  # Delegate to ResourceIdentifier
30
21
  # rubocop:disable Lint/UnusedMethodArgument
31
22
  def serialize_resource_identifier(
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class Sort
6
+ # Compares two values for stable sort ordering. Ruby 3.4+ TrueClass#<=> returns nil
7
+ # when operands differ (true <=> false), so booleans are normalized before <=>.
8
+ module Compare
9
+ module_function
10
+
11
+ def compare(value_a, value_b)
12
+ return 0 if value_a.nil? && value_b.nil?
13
+ return -1 if value_a.nil?
14
+ return 1 if value_b.nil?
15
+
16
+ comparable(value_a) <=> comparable(value_b) ||
17
+ raise_incomparable(value_a, value_b)
18
+ end
19
+
20
+ def comparable(value)
21
+ return value ? 1 : 0 if value in TrueClass | FalseClass
22
+
23
+ value
24
+ end
25
+
26
+ def raise_incomparable(value_a, value_b)
27
+ detail = "#{value_a.class.name} (#{value_a.inspect}), #{value_b.class.name} (#{value_b.inspect})"
28
+ raise ArgumentError, "incomparable sort values: #{detail}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class Sort
6
+ class Field
7
+ class Error < StandardError; end
8
+
9
+ attr_reader :name, :direction
10
+
11
+ def initialize(model_class, sort_field)
12
+ @model_class = model_class
13
+ @name = sort_field.delete_prefix("-")
14
+ @direction = sort_field.start_with?("-") ? :desc : :asc
15
+ end
16
+
17
+ def virtual? = !(column? || scope?)
18
+
19
+ def apply(scope)
20
+ raise Error, "cannot chain a virtual sort" if virtual?
21
+
22
+ if column?
23
+ scope.order(name => direction)
24
+ else
25
+ scope.public_send(name.to_sym, direction)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def column? = @model_class.column_names.include?(name.to_s)
32
+ def scope? = @model_class.respond_to?(name.to_sym)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sort/field"
4
+ require_relative "sort/compare"
5
+
6
+ module JSONAPI
7
+ module Support
8
+ # Applies sort parameters to a scope, handling database column sorts,
9
+ # scope-based sorts, and virtual attribute sorts (in-memory).
10
+ #
11
+ # Validates sort_params against resource_class.permitted_sortable_fields
12
+ # eagerly in the constructor. Raises InvalidFieldError if any fields are
13
+ # not permitted — callers never need to validate separately.
14
+ #
15
+ # Virtual sorts (resource instance methods) cannot be applied at the
16
+ # database level, so they are collected and applied last as an in-memory
17
+ # sort over the materialised result set. This means virtual sorts always
18
+ # take precedence over column/scope sorts regardless of the order the
19
+ # user specified them in.
20
+ class Sort
21
+ class InvalidFieldError < JSONAPI::Error
22
+ attr_reader :invalid_fields
23
+
24
+ def initialize(invalid_fields)
25
+ @invalid_fields = invalid_fields
26
+ super("Invalid sort fields: #{invalid_fields.join(", ")}")
27
+ end
28
+ end
29
+
30
+ def initialize(sort_params:, resource_class:)
31
+ @fields = build_fields(sort_params, resource_class)
32
+ end
33
+
34
+ def apply(scope)
35
+ return scope if @fields.empty?
36
+
37
+ virtual, db = @fields.partition(&:virtual?)
38
+ scope = db.reduce(scope) { |acc, field| field.apply(acc) }
39
+ virtual.any? ? apply_virtual_sorting(scope, virtual) : scope
40
+ end
41
+
42
+ def virtual_fields?
43
+ @fields.any?(&:virtual?)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :resource_class
49
+
50
+ def build_fields(sort_params, resource_class)
51
+ return [] if sort_params.empty?
52
+
53
+ @resource_class = resource_class
54
+ validate!(sort_params, resource_class)
55
+ sort_params.map { Field.new(resource_class.model_class, it) }
56
+ end
57
+
58
+ def validate!(sort_params, resource_class)
59
+ permitted = resource_class.permitted_sortable_fields.map(&:to_s)
60
+ invalid = sort_params.filter_map do |sort_field|
61
+ name = sort_field.delete_prefix("-")
62
+ name unless permitted.include?(name.to_s)
63
+ end
64
+ raise InvalidFieldError, invalid if invalid.any?
65
+ end
66
+
67
+ def apply_virtual_sorting(scope, virtual_fields)
68
+ scope.to_a.sort { |a, b| compare_by_fields(a, b, virtual_fields) }
69
+ end
70
+
71
+ def compare_by_fields(record_a, record_b, fields)
72
+ fields.each do |field|
73
+ value_a = virtual_value(record_a, field.name)
74
+ value_b = virtual_value(record_b, field.name)
75
+ comparison = Compare.compare(value_a, value_b)
76
+ next if comparison.zero?
77
+
78
+ return field.direction == :desc ? -comparison : comparison
79
+ end
80
+ 0
81
+ end
82
+
83
+ def virtual_value(record, field_name)
84
+ instance = resource_class.new(record, {})
85
+ field_sym = field_name.to_sym
86
+ instance.respond_to?(field_sym, false) ? instance.public_send(field_sym) : nil
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "2.1.5"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/json_api.rb CHANGED
@@ -28,7 +28,6 @@ end
28
28
  require "json_api/resources/resource"
29
29
  require "json_api/resources/resource_loader"
30
30
  require "json_api/support/type_conversion"
31
- require "json_api/support/sort_parsing"
32
31
  require "json_api/support/resource_identifier"
33
32
  require "json_api/support/relationship_helpers"
34
33
  require "json_api/support/param_helpers"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.5
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp
@@ -178,7 +178,6 @@ files:
178
178
  - lib/json_api/support/concerns/pagination.rb
179
179
  - lib/json_api/support/concerns/polymorphic_filters.rb
180
180
  - lib/json_api/support/concerns/regular_filters.rb
181
- - lib/json_api/support/concerns/sorting.rb
182
181
  - lib/json_api/support/correlation_id.rb
183
182
  - lib/json_api/support/filter_parsing.rb
184
183
  - lib/json_api/support/header_warning_subscriber.rb
@@ -192,8 +191,9 @@ files:
192
191
  - lib/json_api/support/relationship_helpers.rb
193
192
  - lib/json_api/support/resource_identifier.rb
194
193
  - lib/json_api/support/responders.rb
195
- - lib/json_api/support/sort_parsing.rb
196
- - lib/json_api/support/sort_value_comparison.rb
194
+ - lib/json_api/support/sort.rb
195
+ - lib/json_api/support/sort/compare.rb
196
+ - lib/json_api/support/sort/field.rb
197
197
  - lib/json_api/support/type_conversion.rb
198
198
  - lib/json_api/testing.rb
199
199
  - lib/json_api/testing/rspec.rb
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json_api/support/sort_value_comparison"
4
-
5
- module JSONAPI
6
- module Support
7
- module Sorting
8
- def apply_sorting(scope)
9
- return scope if sort_params.empty?
10
-
11
- has_virtual_sorts = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
12
- has_virtual_sorts ? apply_virtual_sorting(scope) : apply_db_sorting(scope)
13
- end
14
-
15
- private
16
-
17
- def apply_virtual_sorting(scope)
18
- records = scope.to_a
19
- apply_mixed_sorting(records, sort_params)
20
- end
21
-
22
- def apply_db_sorting(scope)
23
- sort_params.each do |sort_field|
24
- direction = RelationshipHelpers.extract_sort_direction(sort_field)
25
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
26
- scope = scope.order(field => direction)
27
- end
28
- scope
29
- end
30
-
31
- def virtual_attribute_sort?(sort_field)
32
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
33
- !model_class.column_names.include?(field.to_s)
34
- end
35
-
36
- def apply_mixed_sorting(records, all_sorts)
37
- records.sort do |a, b|
38
- compare_by_sort_criteria(a, b, all_sorts)
39
- end
40
- end
41
-
42
- def compare_by_sort_criteria(record_a, record_b, all_sorts)
43
- all_sorts.each do |sort_field|
44
- direction = RelationshipHelpers.extract_sort_direction(sort_field)
45
- field = RelationshipHelpers.extract_sort_field_name(sort_field)
46
- value_a = fetch_sort_value(record_a, field)
47
- value_b = fetch_sort_value(record_b, field)
48
- comparison = compare_values(value_a, value_b)
49
- next if comparison.zero?
50
-
51
- return direction == :desc ? -comparison : comparison
52
- end
53
- 0
54
- end
55
-
56
- def fetch_sort_value(record, field)
57
- return fetch_column_value(record, field) if model_class.column_names.include?(field.to_s)
58
-
59
- fetch_virtual_attribute_value(record, field)
60
- end
61
-
62
- def fetch_column_value(record, field)
63
- return record.public_send(field.to_sym) if record.respond_to?(field.to_sym)
64
- return nil unless record.respond_to?(:attributes) && record.attributes.is_a?(Hash)
65
-
66
- record.attributes[field.to_s] || record.attributes[field.to_sym]
67
- end
68
-
69
- def fetch_virtual_attribute_value(record, field)
70
- definition_instance = definition.new(record, {})
71
- fetch_virtual_value(definition_instance, field)
72
- end
73
-
74
- def fetch_virtual_value(definition_instance, field)
75
- field_sym = field.to_sym
76
- return definition_instance.public_send(field_sym) if definition_instance.respond_to?(field_sym, false)
77
-
78
- nil
79
- end
80
-
81
- def compare_values(value_a, value_b)
82
- SortValueComparison.compare(value_a, value_b)
83
- end
84
- end
85
- end
86
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module SortParsing
5
- module_function
6
-
7
- def parse_sort_field(sort_field)
8
- descending = sort_field.start_with?("-")
9
- field = descending ? sort_field[1..] : sort_field
10
- { field:, direction: descending ? :desc : :asc }
11
- end
12
-
13
- def extract_sort_field_name(sort_field)
14
- parse_sort_field(sort_field)[:field]
15
- end
16
-
17
- def extract_sort_direction(sort_field)
18
- parse_sort_field(sort_field)[:direction]
19
- end
20
- end
21
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module Support
5
- # Compares two values for stable sort ordering. Ruby 3.4+ TrueClass#<=> returns nil
6
- # when operands differ (true <=> false), so booleans are normalized before <=>.
7
- module SortValueComparison
8
- module_function
9
-
10
- def compare(value_a, value_b)
11
- return 0 if value_a.nil? && value_b.nil?
12
- return -1 if value_a.nil?
13
- return 1 if value_b.nil?
14
-
15
- value_a = value_a ? 1 : 0 if value_a in TrueClass | FalseClass
16
- value_b = value_b ? 1 : 0 if value_b in TrueClass | FalseClass
17
-
18
- result = value_a <=> value_b
19
- return result unless result.nil?
20
-
21
- detail = "#{value_a.class.name} (#{value_a.inspect}), #{value_b.class.name} (#{value_b.inspect})"
22
- raise ArgumentError, "incomparable sort values: #{detail}"
23
- end
24
- end
25
- end
26
- end