lexorank 0.1.3 → 0.3.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: 4643e7db16d6b8ed5dcb807f80d2350663f168b8b66e0e81791d1c746fd1a17a
4
- data.tar.gz: 2fdf4f043ef14b8fab235422f409b151fc1117000ea304c2612ccb0e71a079ca
3
+ metadata.gz: 705faccc54d5d09c0ce7e8239d777b79ebe9b8857ea2956c6b7f9378f0b027d6
4
+ data.tar.gz: b61be364fc9dfffb76947d4ad8ae6462ac8d70490a33262bdb96ac8601e1eb7f
5
5
  SHA512:
6
- metadata.gz: 27fb2b029e5eab9088a1236b0b0cb18688e0a8efdee64cfedee869dd1b72a7257056757d59989dd6a0ffc8164af77282108258d066fd8462a1dfc0f074128b61
7
- data.tar.gz: 9dda1742be04a290c3b68defcbb6f88302665ee01ff0f5cbb4086a62a004ee95e7ebe381f1280414dee6d84a8810a177e712ae4f86eee4c8e243a40a5c20d216
6
+ metadata.gz: 188136b56717efec3aa8dedcdd9f977506a21677814dbe2d85488fa6f2154abe877f1412a4aaab03c1aae7b8f8badacd1982b2f77b12718893594133e10e10ef
7
+ data.tar.gz: afd223cb7e8a444cc0428cfab024e89044a9918cfe013972ea65d2c4250f4db70b6b5653d3d5ba989ad548f33f6fda34b6730f3a5053872e0f705f56187d7185
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Richard Böhme
3
+ Copyright (c) 2024 Richard Böhme
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,92 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'lexorank'
4
+ require 'lexorank/ranking'
2
5
  require 'active_support/concern'
3
6
 
4
7
  module Lexorank::Rankable
5
8
  extend ActiveSupport::Concern
6
9
 
7
10
  module ClassMethods
8
- attr_reader :ranking_column, :ranking_group_by
11
+ attr_reader :lexorank_ranking
9
12
 
10
- def rank!(field: :rank, group_by: nil)
11
- @ranking_column = check_column(field)
12
- if group_by
13
- @ranking_group_by = check_column(group_by)
14
- unless @ranking_group_by
15
- warn "The supplied grouping by \"#{group_by}\" is neither a column nor an association of the model!"
16
- end
17
- end
13
+ def rank!(field: :rank, group_by: nil, advisory_lock: {})
14
+ @lexorank_ranking = Lexorank::Ranking.new(record_class: self, field: field, group_by: group_by, advisory_lock: advisory_lock)
15
+ lexorank_ranking.validate!
18
16
 
19
- if @ranking_column
20
- self.scope :ranked, ->(direction: :asc) { where.not("#{field}": nil).order("#{field}": direction) }
21
- self.include Lexorank
22
- self.include InstanceMethods
23
- else
24
- warn "The supplied ranking column \"#{field}\" is not a column of the model!"
17
+ if lexorank_ranking.field
18
+ scope :ranked, ->(direction: :asc) { where.not("#{lexorank_ranking.field}": nil).order("#{lexorank_ranking.field}": direction) }
19
+ include InstanceMethods
25
20
  end
26
21
  end
27
-
28
- private
29
-
30
- def check_column(column_name)
31
- return unless column_name
32
- # This requires an active connection... do we want this?
33
- if self.columns.map(&:name).include?(column_name.to_s)
34
- column_name
35
- # This requires rank! to be after the specific association
36
- elsif (association = self.reflect_on_association(column_name))
37
- association.foreign_key.to_sym
38
- end
39
- end
40
-
41
22
  end
42
23
 
43
24
  module InstanceMethods
44
- def move_to_top
45
- move_to(0)
25
+ def move_to_top(&)
26
+ move_to(0, &)
46
27
  end
47
28
 
48
- def move_to(position)
49
- collection = self.class.ranked
50
- if self.class.ranking_group_by.present?
51
- collection = collection.where("#{self.class.ranking_group_by}": send(self.class.ranking_group_by))
52
- end
53
-
54
- # exceptions:
55
- # move to the beginning (aka move to position 0)
56
- # move to end (aka position = collection.size - 1)
57
- # when moving to the end of the collection the offset and limit statement automatically handles
58
- # that 'after' is nil which is the same like [collection.last, nil]
59
- before, after =
60
- if position == 0
61
- [nil, collection.first]
62
- else
63
- collection.where.not(id: self.id).offset(position - 1).limit(2)
64
- end
65
-
66
- rank =
67
- if self == after && self.send(self.class.ranking_column).present?
68
- self.send(self.class.ranking_column)
69
- else
70
- value_between(before&.send(self.class.ranking_column), after&.send(self.class.ranking_column))
71
- end
29
+ def move_to_end(&)
30
+ self.class.lexorank_ranking.move_to(self, :last, &)
31
+ end
72
32
 
73
- self.send("#{self.class.ranking_column}=", rank)
33
+ def move_to(position, &)
34
+ self.class.lexorank_ranking.move_to(self, position, &)
74
35
  end
75
36
 
76
37
  def move_to!(position)
77
- move_to(position)
78
- save
38
+ move_to(position) do
39
+ save
40
+ end
79
41
  end
80
42
 
81
43
  def move_to_top!
82
- move_to!(0)
44
+ move_to_top do
45
+ save
46
+ end
47
+ end
48
+
49
+ def move_to_end!
50
+ move_to_end do
51
+ save
52
+ end
83
53
  end
84
54
 
85
55
  def no_rank?
86
- !self.send(self.class.ranking_column)
56
+ !send(self.class.lexorank_ranking.field)
87
57
  end
88
58
  end
89
-
90
59
  end
91
- ActiveRecord::Base.send(:include, Lexorank::Rankable)
92
60
 
61
+ ActiveRecord::Base.include Lexorank::Rankable
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lexorank::Ranking
4
+ include Lexorank
5
+
6
+ attr_reader :record_class, :field, :group_by, :advisory_lock_config
7
+
8
+ def initialize(record_class:, field:, group_by:, advisory_lock:)
9
+ @record_class = record_class
10
+ @field = field
11
+ @group_by = process_group_by_column_name(group_by)
12
+ @advisory_lock_config = { enabled: record_class.respond_to?(:with_advisory_lock) }.merge(advisory_lock)
13
+ end
14
+
15
+ def validate!
16
+ if advisory_lock_config[:enabled] && !record_class.respond_to?(:with_advisory_lock)
17
+ raise(
18
+ Lexorank::InvalidConfigError,
19
+ "Cannot enable advisory lock if #{record_class.name} does not respond to #with_advisory_lock. " \
20
+ 'Consider installing the with_advisory_lock gem (https://rubygems.org/gems/with_advisory_lock).'
21
+ )
22
+ end
23
+
24
+ unless field
25
+ raise(
26
+ Lexorank::InvalidConfigError,
27
+ 'The supplied ":field" option cannot be "nil"!'
28
+ )
29
+ end
30
+ end
31
+
32
+ def move_to(instance, position)
33
+ if block_given? && advisory_locks_enabled?
34
+ return with_lock_if_enabled(instance) do
35
+ move_to(instance, position)
36
+ yield
37
+ end
38
+ end
39
+
40
+ collection = record_class.ranked
41
+ if group_by.present?
42
+ collection = collection.where("#{group_by}": instance.send(group_by))
43
+ end
44
+
45
+ # exceptions:
46
+ # move to the beginning (aka move to position 0)
47
+ # move to end (aka position = collection.size - 1)
48
+ # when moving to the end of the collection the offset and limit statement automatically handles
49
+ # that 'after' is nil which is the same like [collection.last, nil]
50
+ before, after =
51
+ if position == :last
52
+ [collection.last, nil]
53
+ elsif position.zero?
54
+ [nil, collection.first]
55
+ else
56
+ collection.where.not(id: instance.id).offset(position - 1).limit(2)
57
+ end
58
+
59
+ # If position >= collection.size both `before` and `after` will be nil. In this case
60
+ # we set before to the last element of the collection
61
+ if before.nil? && after.nil?
62
+ before = collection.last
63
+ end
64
+
65
+ rank =
66
+ if (self == after && send(field).present?) || (before == self && after.nil?)
67
+ send(field)
68
+ else
69
+ value_between(before&.send(field), after&.send(field))
70
+ end
71
+
72
+ instance.send(:"#{field}=", rank)
73
+
74
+ if block_given?
75
+ yield
76
+ else
77
+ rank
78
+ end
79
+ end
80
+
81
+ def with_lock_if_enabled(instance, &)
82
+ if advisory_locks_enabled?
83
+ advisory_lock_options = advisory_lock_config.except(:enabled, :lock_name)
84
+
85
+ record_class.with_advisory_lock(advisory_lock_name(instance), **advisory_lock_options, &)
86
+ else
87
+ yield
88
+ end
89
+ end
90
+
91
+ def advisory_lock_name(instance)
92
+ if advisory_lock_config[:lock_name].present?
93
+ advisory_lock_config[:lock_name].(instance)
94
+ else
95
+ "#{record_class.table_name}_update_#{field}".tap do |name|
96
+ if group_by.present?
97
+ name << "_group_#{instance.send(group_by)}"
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def advisory_locks_enabled?
104
+ record_class.respond_to?(:with_advisory_lock) && advisory_lock_config[:enabled]
105
+ end
106
+
107
+ private
108
+
109
+ def process_group_by_column_name(name)
110
+ # This requires rank! to be after the specific association
111
+ if name && (association = record_class.reflect_on_association(name))
112
+ association.foreign_key.to_sym
113
+ else
114
+ name
115
+ end
116
+ end
117
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lexorank
2
- VERSION = '0.1.3'
4
+ VERSION = '0.3.0'
3
5
  end
data/lib/lexorank.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'lexorank/version'
2
4
 
3
5
  # Inspired by https://github.com/DevStarSJ/LexoRank/blob/master/lexo_rank.rb licensed under
@@ -24,13 +26,14 @@ require 'lexorank/version'
24
26
  # SOFTWARE.
25
27
  module Lexorank
26
28
  class InvalidRankError < StandardError; end
29
+ class InvalidConfigError < StandardError; end
27
30
 
28
- MIN_CHAR = '0'.freeze
29
- MAX_CHAR = 'z'.freeze
31
+ MIN_CHAR = '0'
32
+ MAX_CHAR = 'z'
30
33
 
31
- def value_between(_before_, _after_)
32
- before = _before_ || MIN_CHAR
33
- after = _after_ || MAX_CHAR
34
+ def value_between(before_, after_)
35
+ before = before_ || MIN_CHAR
36
+ after = after_ || MAX_CHAR
34
37
 
35
38
  rank = ''
36
39
 
@@ -64,8 +67,10 @@ module Lexorank
64
67
  #
65
68
  # Please report if you have another opinion about that or if you reached the exception! (of course you can force it by using `value_between(nil, '0')`)
66
69
  if rank >= after
67
- raise InvalidRankError, "This rank should not be achievable using the Lexorank::Rankable module! Please report to https://github.com/richardboehme/lexorank/issues! " +
68
- "The supplied ranks were #{_before_.inspect} and #{_after_.inspect}. Please include those in the issue description."
70
+ raise InvalidRankError,
71
+ 'This rank should not be achievable using the Lexorank::Rankable module! ' \
72
+ 'Please report to https://github.com/richardboehme/lexorank/issues! ' \
73
+ "The supplied ranks were #{before_.inspect} and #{after_.inspect}. Please include those in the issue description."
69
74
  end
70
75
  rank
71
76
  end
@@ -75,8 +80,7 @@ module Lexorank
75
80
  middle_ascii.chr
76
81
  end
77
82
 
78
- def get_char(str, i, default_char)
79
- i >= str.length ? default_char : str[i]
83
+ def get_char(string, index, default_char)
84
+ index >= string.length ? default_char : string[index]
80
85
  end
81
-
82
86
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lexorank
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Böhme
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-16 00:00:00.000000000 Z
11
+ date: 2024-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activesupport
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: activerecord
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -39,104 +25,20 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
- name: shoulda-context
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: sqlite3
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: minitest
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: minitest-reporters
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: m
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: pry
28
+ name: activesupport
127
29
  requirement: !ruby/object:Gem::Requirement
128
30
  requirements:
129
31
  - - ">="
130
32
  - !ruby/object:Gem::Version
131
33
  version: '0'
132
- type: :development
34
+ type: :runtime
133
35
  prerelease: false
134
36
  version_requirements: !ruby/object:Gem::Requirement
135
37
  requirements:
136
38
  - - ">="
137
39
  - !ruby/object:Gem::Version
138
40
  version: '0'
139
- description:
41
+ description:
140
42
  email:
141
43
  - richard.boehme1999@gmail.com
142
44
  executables: []
@@ -146,12 +48,17 @@ files:
146
48
  - LICENSE
147
49
  - lib/lexorank.rb
148
50
  - lib/lexorank/rankable.rb
51
+ - lib/lexorank/ranking.rb
149
52
  - lib/lexorank/version.rb
150
53
  homepage: https://github.com/richardboehme/lexorank
151
54
  licenses:
152
55
  - MIT
153
- metadata: {}
154
- post_install_message:
56
+ metadata:
57
+ rubygems_mfa_required: 'true'
58
+ homepage_uri: https://github.com/richardboehme/lexorank
59
+ source_code_uri: https://github.com/richardboehme/lexorank
60
+ changelog_uri: https://github.com/richardboehme/lexorank/blob/main/CHANGELOG.md
61
+ post_install_message:
155
62
  rdoc_options: []
156
63
  require_paths:
157
64
  - lib
@@ -159,15 +66,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
66
  requirements:
160
67
  - - ">="
161
68
  - !ruby/object:Gem::Version
162
- version: 2.3.0
69
+ version: 3.1.0
163
70
  required_rubygems_version: !ruby/object:Gem::Requirement
164
71
  requirements:
165
72
  - - ">="
166
73
  - !ruby/object:Gem::Version
167
74
  version: '0'
168
75
  requirements: []
169
- rubygems_version: 3.2.15
170
- signing_key:
76
+ rubygems_version: 3.5.9
77
+ signing_key:
171
78
  specification_version: 4
172
79
  summary: Store order of your models by using lexicographic sorting.
173
80
  test_files: []