lexorank 0.1.2 → 0.2.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: 016c148a28989a35ab6afd0184625900cbec71dac5dcfa981cac45403c9aa0d5
4
- data.tar.gz: 46cfe89fa41fb20b511235122b8511702ddfba532c1d475d7dfe2cec337a2e7c
3
+ metadata.gz: 422a0db95be12bd9cffe35d7f549b1ebb71dd5de9751424a3794fd18dc2065c1
4
+ data.tar.gz: 8d00dd0cb9331d2a5152bb9f68c3ba7a29ac84de4732bc03bf5bc78a946c8a58
5
5
  SHA512:
6
- metadata.gz: 969a110cb1cc3c60ea599b6b747326a6f86c57de0a261c0f369d0691108c59f757e8d0d12b2d669bc2dbfefe275dd83e1d851f6f6b181ac1dec917388f54458c
7
- data.tar.gz: 249348edc5ffeb92cc42a1d98e585c671560443d53e5a150b64d5674f49ceaf7857e3ca80208a8da0de20ca380aa5598fa5726672842c833294d2710d4a68e4a
6
+ metadata.gz: c49a7c38ed0853cdddc5fd1664d085ad08b8b2d91c00e8920da4a19078c6949dd29608bd92013eb31fabd2b1b46191c9ef589fb53629196f0582b53d132ca0f6
7
+ data.tar.gz: 6e303a53f9185913313d20ef37a9a549b1323c174c98cda5f3533e8bf16e1188ece014fa0fbbf8545b55e040b316f671aa960b99adae3b184ab05e00c6783026
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
- # if item is currently in front of the index we just use position otherwise position - 1
64
- # if the item has no rank we use position - 1
65
- position -= 1 if !self.send(self.class.ranking_column) || collection.map(&:id).index(self.id) > position
66
- collection.offset(position).limit(2)
67
- end
68
-
69
- rank =
70
- if self == after && self.send(self.class.ranking_column).present?
71
- self.send(self.class.ranking_column)
72
- else
73
- value_between(before&.send(self.class.ranking_column), after&.send(self.class.ranking_column))
74
- end
29
+ def move_to_end(&)
30
+ self.class.lexorank_ranking.move_to(self, :last, &)
31
+ end
75
32
 
76
- self.send("#{self.class.ranking_column}=", rank)
33
+ def move_to(position, &)
34
+ self.class.lexorank_ranking.move_to(self, position, &)
77
35
  end
78
36
 
79
37
  def move_to!(position)
80
- move_to(position)
81
- save
38
+ move_to(position) do
39
+ save
40
+ end
82
41
  end
83
42
 
84
43
  def move_to_top!
85
- move_to!(0)
44
+ move_to_top do
45
+ save
46
+ end
86
47
  end
87
48
 
88
- end
49
+ def move_to_end!
50
+ move_to_end do
51
+ save
52
+ end
53
+ end
89
54
 
55
+ def no_rank?
56
+ !send(self.class.lexorank_ranking.field)
57
+ end
58
+ end
90
59
  end
91
- ActiveRecord::Base.send(:include, Lexorank::Rankable)
92
60
 
61
+ ActiveRecord::Base.include Lexorank::Rankable
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lexorank::Ranking
4
+ include Lexorank
5
+
6
+ attr_reader :record_class, :original_field, :field, :original_group_by, :group_by, :advisory_lock_config
7
+
8
+ def initialize(record_class:, field:, group_by:, advisory_lock:)
9
+ @record_class = record_class
10
+ @original_field = field
11
+ @field = process_column_name(field)
12
+ @original_group_by = group_by
13
+ @group_by = process_group_by_column_name(group_by)
14
+ @advisory_lock_config = { enabled: record_class.respond_to?(:with_advisory_lock) }.merge(advisory_lock)
15
+ end
16
+
17
+ def validate!
18
+ if advisory_lock_config[:enabled] && !record_class.respond_to?(:with_advisory_lock)
19
+ raise(
20
+ Lexorank::InvalidConfigError,
21
+ "Cannot enable advisory lock if #{record_class.name} does not respond to #with_advisory_lock. " \
22
+ 'Consider installing the with_advisory_lock gem (https://rubygems.org/gems/with_advisory_lock).'
23
+ )
24
+ end
25
+
26
+ unless @field
27
+ # TODO: Make this raise an error. Supplying an invalid column should raise.
28
+ warn "The supplied ranking column \"#{@original_field}\" is not a column of the model!"
29
+ end
30
+
31
+ if original_group_by && !group_by
32
+ warn "The supplied grouping by \"#{original_group_by}\" is neither a column nor an association of the model!"
33
+ end
34
+ end
35
+
36
+ def move_to(instance, position)
37
+ if block_given? && advisory_locks_enabled?
38
+ return with_lock_if_enabled(instance) do
39
+ move_to(instance, position)
40
+ yield
41
+ end
42
+ end
43
+
44
+ collection = record_class.ranked
45
+ if group_by.present?
46
+ collection = collection.where("#{group_by}": instance.send(group_by))
47
+ end
48
+
49
+ # exceptions:
50
+ # move to the beginning (aka move to position 0)
51
+ # move to end (aka position = collection.size - 1)
52
+ # when moving to the end of the collection the offset and limit statement automatically handles
53
+ # that 'after' is nil which is the same like [collection.last, nil]
54
+ before, after =
55
+ if position == :last
56
+ [collection.last, nil]
57
+ elsif position.zero?
58
+ [nil, collection.first]
59
+ else
60
+ collection.where.not(id: instance.id).offset(position - 1).limit(2)
61
+ end
62
+
63
+ # If position >= collection.size both `before` and `after` will be nil. In this case
64
+ # we set before to the last element of the collection
65
+ if before.nil? && after.nil?
66
+ before = collection.last
67
+ end
68
+
69
+ rank =
70
+ if (self == after && send(field).present?) || (before == self && after.nil?)
71
+ send(field)
72
+ else
73
+ value_between(before&.send(field), after&.send(field))
74
+ end
75
+
76
+ instance.send(:"#{field}=", rank)
77
+
78
+ if block_given?
79
+ yield
80
+ else
81
+ rank
82
+ end
83
+ end
84
+
85
+ def with_lock_if_enabled(instance, &)
86
+ if advisory_locks_enabled?
87
+ advisory_lock_options = advisory_lock_config.except(:enabled, :lock_name)
88
+
89
+ record_class.with_advisory_lock(advisory_lock_name(instance), **advisory_lock_options, &)
90
+ else
91
+ yield
92
+ end
93
+ end
94
+
95
+ def advisory_lock_name(instance)
96
+ if advisory_lock_config[:lock_name].present?
97
+ advisory_lock_config[:lock_name].(instance)
98
+ else
99
+ "#{record_class.table_name}_update_#{field}".tap do |name|
100
+ if group_by.present?
101
+ name << "_group_#{instance.send(group_by)}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def advisory_locks_enabled?
108
+ record_class.respond_to?(:with_advisory_lock) && advisory_lock_config[:enabled]
109
+ end
110
+
111
+ private
112
+
113
+ def process_column_name(name)
114
+ return unless name
115
+
116
+ # This requires an active connection... do we want this?
117
+ if record_class.columns.map(&:name).include?(name.to_s)
118
+ name
119
+ end
120
+ end
121
+
122
+ def process_group_by_column_name(name)
123
+ processed_name = process_column_name(name)
124
+
125
+ # This requires rank! to be after the specific association
126
+ if name && !processed_name && (association = record_class.reflect_on_association(name))
127
+ association.foreign_key.to_sym
128
+ else
129
+ processed_name
130
+ end
131
+ end
132
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lexorank
2
- VERSION = '0.1.2'
4
+ VERSION = '0.2.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.2
4
+ version: 0.2.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-03-08 00:00:00.000000000 Z
11
+ date: 2024-08-16 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.3
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: []