activekit 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/active_kit/engine.rb +9 -0
- data/lib/active_kit/sequence/sequenceable.rb +109 -0
- data/lib/active_kit/sequence/sequencer.rb +69 -0
- data/lib/active_kit/sequence/wordbook.rb +60 -0
- data/lib/active_kit/version.rb +1 -1
- data/lib/active_kit.rb +4 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d79cd1518b90bb64257a77318f59cfa3f2bfa9bb58a45ead89ddfa5bcb08b001
|
4
|
+
data.tar.gz: c8891e2351e0940b765a0a5c296403f2885fac4b64db00b822d32707f84b156c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83975dd7d4e472532c0b1afa3afcf2aebda640093f6cedb325c9b69adc50f8216e8cc59edaea6f2d923320be8e7409e026c3d36ba48f29469acec41a97bf609b
|
7
|
+
data.tar.gz: 6bb3596f95f6893e9ce5a342b96ac22ed2d72cf36866900fa75fedb8a6a397020c0d3295160f1ddca8bee1a36921adf3e055d0e65b1b7d6d60caa6dc7975401b
|
data/lib/active_kit/engine.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace ActiveKit
|
4
|
+
config.eager_load_namespaces << ActiveKit
|
5
|
+
|
6
|
+
initializer "active_kit.sequence" do
|
7
|
+
require "active_kit/sequence/sequenceable"
|
8
|
+
|
9
|
+
ActiveSupport.on_load(:active_record) do
|
10
|
+
include ActiveKit::Sequence::Sequenceable
|
11
|
+
end
|
12
|
+
end
|
4
13
|
end
|
5
14
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module ActiveKit
|
4
|
+
module Sequence
|
5
|
+
module Sequenceable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
end
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
# Usage Options
|
13
|
+
# sequence_attribute :name
|
14
|
+
# sequence_attribute :name, :positioning_method
|
15
|
+
# sequence_attribute :name, :positioning_method, updater: {}
|
16
|
+
# sequence_attribute :name, :positioning_method, updater: { on: {} }
|
17
|
+
# sequence_attribute :name, :positioning_method, updater: { via: :assoc, on: {} }
|
18
|
+
# sequence_attribute :name, :positioning_method, updater: { via: {}, on: {} }
|
19
|
+
# Note: :on and :via in :updater can accept nested associations.
|
20
|
+
def sequence_attribute(name, positioning_method, **options)
|
21
|
+
name = name.to_sym
|
22
|
+
positioning_method = positioning_method.to_sym
|
23
|
+
options.deep_symbolize_keys!
|
24
|
+
|
25
|
+
unless self.respond_to?(:sequencer)
|
26
|
+
define_singleton_method :sequencer do
|
27
|
+
@sequencer ||= ActiveSequence::Sequencer.new(current_class: self)
|
28
|
+
end
|
29
|
+
|
30
|
+
has_many :sequence_attributes, as: :record, dependent: :destroy, class_name: "ActiveSequence::Attribute"
|
31
|
+
# scope :order_sequence, -> (options_hash) { includes(:sequence_attributes).where(sequence_attributes: { name: name.to_s }).order("sequence_attributes.value": :asc) }
|
32
|
+
end
|
33
|
+
|
34
|
+
set_active_sequence_create_callbacks(attribute_name: name)
|
35
|
+
set_active_sequence_commit_callbacks(attribute_name: name, positioning_method: positioning_method, updater: options.delete(:updater))
|
36
|
+
|
37
|
+
sequencer.add_attribute(name: name, options: options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_active_sequence_create_callbacks(attribute_name:)
|
41
|
+
before_create do
|
42
|
+
self.sequence_attributes.find_or_initialize_by(name: attribute_name)
|
43
|
+
logger.info "ActiveSequence - Creating Sequence attribute '#{attribute}' from #{self.class.name}: Done."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_active_sequence_commit_callbacks(attribute_name:, positioning_method:, updater:)
|
48
|
+
updater = updater || {}
|
49
|
+
|
50
|
+
if updater.empty?
|
51
|
+
after_commit do
|
52
|
+
position = positioning_method ? self.public_send(positioning_method) : nil
|
53
|
+
self.class.sequencer.update(record: self, attribute_name: attribute_name, position: position)
|
54
|
+
logger.info "ActiveSequence - Sequencing from #{self.class.name}: Done."
|
55
|
+
end
|
56
|
+
else
|
57
|
+
raise ":updater should be a hash while setting sequence_attribute. " unless updater.is_a?(Hash)
|
58
|
+
raise ":on in :updater should be a hash while setting sequence_attribute. " if updater.key?(:on) && !updater[:on].is_a?(Hash)
|
59
|
+
raise "Cannot use :via without :on in :updater while setting sequence_attribute. " if updater.key?(:via) && !updater.key?(:on)
|
60
|
+
|
61
|
+
updater_via = updater.delete(:via)
|
62
|
+
updater_on = updater.delete(:on) || updater
|
63
|
+
|
64
|
+
base_klass = search_base_klass(self.class.name, updater_via)
|
65
|
+
klass = reflected_klass(base_klass, updater_on.key)
|
66
|
+
klass.constantize.class_eval do
|
67
|
+
after_commit do
|
68
|
+
inverse_assoc = search_inverse_assoc(self, updater_on)
|
69
|
+
position = positioning_method ? self.public_send(positioning_method) : nil
|
70
|
+
if inverse_assoc.respond_to?(:each)
|
71
|
+
inverse_assoc.each { |instance| instance.class.sequencer.update(record: instance, attribute_name: attribute_name, position: position) }
|
72
|
+
else
|
73
|
+
inverse_assoc.class.sequencer.update(record: inverse_assoc, attribute_name: attribute_name, position: position)
|
74
|
+
end
|
75
|
+
logger.info "ActiveSequence - Sequencing from #{self.class.name}: Done."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def search_base_klass(classname, updater_via)
|
82
|
+
if updater_via.blank?
|
83
|
+
classname
|
84
|
+
elsif updater_via.is_a? Symbol
|
85
|
+
reflected_klass(classname, updater_via)
|
86
|
+
elsif updater_via.is_a? Hash
|
87
|
+
klass = reflected_klass(classname, updater_via.key)
|
88
|
+
updater_via.value.is_a?(Hash) ? search_base_klass(klass, updater_via.value) : reflected_klass(klass, updater_via.value)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def reflected_klass(classname, key)
|
93
|
+
klass = classname.constantize.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[key]
|
94
|
+
raise "Could not find reflected klass for classname '#{classname}' and key '#{key}' while setting sequence_attribute" unless klass
|
95
|
+
klass
|
96
|
+
end
|
97
|
+
|
98
|
+
def search_inverse_assoc(klass_object, updater_on)
|
99
|
+
if updater_on.value.is_a?(Hash)
|
100
|
+
klass_object = klass_object.public_send(updater_on.value.key)
|
101
|
+
search_inverse_assoc(klass_object, updater_on.value)
|
102
|
+
else
|
103
|
+
klass_object.public_send(updater_on.value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Sequence
|
3
|
+
class Sequencer
|
4
|
+
attr_reader :defined_attributes
|
5
|
+
|
6
|
+
def initialize(current_class:)
|
7
|
+
@current_class = current_class
|
8
|
+
|
9
|
+
@defined_attributes = {}
|
10
|
+
@wordbook = Wordbook.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(record:, attribute_name:, position:)
|
14
|
+
attribute = Attribute.find_by(record: record, name: attribute_name)
|
15
|
+
Attribute.create!(record: record, name: attribute_name) unless attribute
|
16
|
+
|
17
|
+
if position
|
18
|
+
raise "position '#{position}' is not a valid unsigned integer value greater than 0." unless position.is_a?(Integer) && position > 0
|
19
|
+
|
20
|
+
attribute_at_position = Attribute.where(record_type: record.class.name, name: attribute_name).order(value: :asc).offset(position - 1).limit(1)
|
21
|
+
attribute
|
22
|
+
|
23
|
+
wordbook = Wordbook.new
|
24
|
+
total_position_count = Attribute.where(record_type: record.class.name, name: attribute_name).order(value: :asc).count
|
25
|
+
if attribute == attribute_at_position(position)
|
26
|
+
return
|
27
|
+
elsif position == 1
|
28
|
+
|
29
|
+
elsif position == total_position_count
|
30
|
+
wordbook.bookmark = attribute_at_position(total_position_count).value
|
31
|
+
|
32
|
+
attribute.value = wordbook.next_word if wordbook.next_word?
|
33
|
+
attribute.save!
|
34
|
+
elsif position > total_position_count
|
35
|
+
maximum_word = Attribute.where(record_type: record.class.name, name: attribute_name).maximum(:value)
|
36
|
+
|
37
|
+
wordbook.bookmark = maximum_word
|
38
|
+
attribute.value = wordbook.next_word if wordbook.next_word?
|
39
|
+
attribute.save!
|
40
|
+
elsif position < total_position_count
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def rebalance_from(position:)
|
47
|
+
ActiveRecord::Base.transaction do
|
48
|
+
wordbook = Wordbook.new
|
49
|
+
Attribute.where(record_type: record.class.name, name: attribute_name).order(value: :asc).offset(position - 1).limit(1)
|
50
|
+
Attribute.where(record_type: record.class.name, name: attribute_name).order(value: :asc).offset(position - 1).each do |attribute|
|
51
|
+
wordbook.bookmark = attribute.value
|
52
|
+
raise "Could not find next word in wordbook while rebalancing" unless wordbook.next_word?
|
53
|
+
|
54
|
+
attribute.value = wordbook.next_word
|
55
|
+
attribute.save!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def attribute_at_position(position)
|
61
|
+
Attribute.where(record_type: record.class.name, name: attribute_name).order(value: :asc).offset(position - 1).limit(1)
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_attribute(name:, options:)
|
65
|
+
@defined_attributes.store(name, options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Sequence
|
3
|
+
class Wordbook
|
4
|
+
attr_accessor :bookmark
|
5
|
+
|
6
|
+
def initialize(base: 36, length: 7, gap: 8, bookmark: nil)
|
7
|
+
@base = base
|
8
|
+
@length = length
|
9
|
+
@gap = gap
|
10
|
+
|
11
|
+
@first_letter = 0.to_s(@base)
|
12
|
+
@first_word = @first_letter.rjust(@length, @first_letter)
|
13
|
+
|
14
|
+
@last_letter = (@base - 1).to_s(@base)
|
15
|
+
@last_word = @last_letter.rjust(@length, @last_letter)
|
16
|
+
|
17
|
+
@bookmark = bookmark || @first_word
|
18
|
+
end
|
19
|
+
|
20
|
+
def previous_word(count: 1)
|
21
|
+
new_word(direction: :previous, count: count)
|
22
|
+
end
|
23
|
+
|
24
|
+
def previous_word?(count: 1)
|
25
|
+
previous_word(count: count).present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_word(count: 1)
|
29
|
+
new_word(direction: :next, count: count)
|
30
|
+
end
|
31
|
+
|
32
|
+
def next_word?(count: 1)
|
33
|
+
next_word(count: count).present?
|
34
|
+
end
|
35
|
+
|
36
|
+
def between_word(word_one:, word_two:)
|
37
|
+
raise "'#{word_one}' is not in range." if (word_one.length > @length) || (word_one < @first_word || word_one > @last_word)
|
38
|
+
raise "'#{word_two}' is not in range." if (word_two.length > @length) || (word_two < @first_word || word_two > @last_word)
|
39
|
+
|
40
|
+
diff = word_one > word_two ? word_one.to_i(@base) - word_two.to_i(@base) : word_two.to_i(@base) - word_one.to_i(@base)
|
41
|
+
between_word = (diff / 2).to_s(@base)
|
42
|
+
between_word.rjust(@length, @first_letter)
|
43
|
+
end
|
44
|
+
|
45
|
+
def between_word?(word_one:, word_two:)
|
46
|
+
between_word(word_one: word_one, word_two: word_two).present?
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def new_word(direction:, count:)
|
52
|
+
word = @bookmark.to_i(@base)
|
53
|
+
word = direction == :next ? word + (count * @gap) : word - (count * @gap)
|
54
|
+
word = word.to_s(@base).rjust(@length, @first_letter)
|
55
|
+
# TODO: raise exception if word is out of bound before first word or after last word.
|
56
|
+
word
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/active_kit/version.rb
CHANGED
data/lib/active_kit.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activekit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- plainsource
|
@@ -51,6 +51,9 @@ files:
|
|
51
51
|
- config/routes.rb
|
52
52
|
- lib/active_kit.rb
|
53
53
|
- lib/active_kit/engine.rb
|
54
|
+
- lib/active_kit/sequence/sequenceable.rb
|
55
|
+
- lib/active_kit/sequence/sequencer.rb
|
56
|
+
- lib/active_kit/sequence/wordbook.rb
|
54
57
|
- lib/active_kit/version.rb
|
55
58
|
- lib/activekit.rb
|
56
59
|
- lib/tasks/active_kit_tasks.rake
|