activekit 0.2.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: 630c8d871f28fedaf525eecbf06fdfb396bba0f62282ec3a8d7d9ef359a833c4
4
- data.tar.gz: 154de328d7cc8e4520ff9f5f5f57ee6b1e73cf0df352d2aaee97387dca9a06a1
3
+ metadata.gz: c2d19ba43a09297c57c1b373864a53bf9b68787fe3a5b46a5167ae178db0800a
4
+ data.tar.gz: fee7a6d8f645787a847fd33a3500d0aade7162a8ff855c45349e45f77d4fe437
5
5
  SHA512:
6
- metadata.gz: 68aaf23851ac2ba3ae9d73eb4474a3c7fe789c7e6f6e61c0d0e85d23a77f159c9e219cd635a28f99cc215f0b4605e43046196ebbf384f172e4ac0bd0f33fce95
7
- data.tar.gz: 0753d639192b574bda48eee23df69d8f23e95d24c6c03f7571b4fb87c28a99a08bfd346a30b7ff0b0a271a890d4878e2cd37c7aa50bf912d0f21d29014e968f1
6
+ metadata.gz: 7b4e68fd3ee58d3c8d2d7a40995a12fcce17f8264548eb7a178422a009706741833c727f723cc8888d66409affbe77770c77eced0d75d1c1249ec9c335a54512
7
+ data.tar.gz: 0b551a885c6e90178aa7711d8c6356b448a57a28d319ce3daf230e93b2c5d92bd3a2a10f70489ee0cd5f9f77f2490cd6962c33426a089d25dc209d4d9f6d73a5
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 plainsource
1
+ Copyright 2023 Timeboard
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -2,7 +2,46 @@
2
2
  Add the essential kit for rails ActiveRecord models and be happy.
3
3
 
4
4
  ## Usage
5
- How to use my plugin.
5
+
6
+ ### Position Attribute
7
+
8
+ Add positioning to your ActiveRecord models.
9
+ Position Attribute provides full positioning functionality using lexicographic ordering for your model database records.
10
+
11
+ You can also add multiple position attributes in one model to store different arrangements.
12
+
13
+ Just create a database column in your model with type :string with index.
14
+ ```ruby
15
+ add_column :products, :arrangement, :string, index: true, before: :created_at
16
+ ```
17
+
18
+ Then define the column name in your model like below.
19
+ ```ruby
20
+ class Product < ApplicationRecord
21
+ position_attribute :arrangement
22
+ end
23
+ ```
24
+
25
+ Creating the record will automatically set it to the last position:
26
+ ```ruby
27
+ product = Product.create(name: "Nice Product")
28
+ ```
29
+
30
+ The following attribute methods will be added to your model object to use:
31
+ ```ruby
32
+ product.arrangement_position = 1 # Set the position
33
+ product.arrangement_position # Access the manually set position
34
+ product.arrangement_position_in_database # Check the position as per database
35
+ product.arrangement_position_options # Can be used in dropdown
36
+ product.arrangement_position_maximum # Check current maximum position
37
+ ```
38
+
39
+ The following class methods will be added to your model class to use:
40
+ ```ruby
41
+ # If a database table already has existing rows, run this to set initial values.
42
+ # You can also run this to manually harmonize a position attribute column.
43
+ Product.harmonize_arrangement!
44
+ ```
6
45
 
7
46
  ## Installation
8
47
  Add this line to your application's Gemfile:
@@ -4,16 +4,16 @@ module ActiveKit
4
4
  config.eager_load_namespaces << ActiveKit
5
5
 
6
6
  initializer "active_kit.add_middleware" do |app|
7
- require "active_kit/base/middleware"
7
+ require "active_kit/position/middleware"
8
8
 
9
- app.middleware.use ActiveKit::Base::Middleware
9
+ app.middleware.use ActiveKit::Position::Middleware
10
10
  end
11
11
 
12
- initializer "active_kit.activekitable" do
13
- require "active_kit/base/activekitable"
12
+ initializer "active_kit.position" do
13
+ require "active_kit/position/positionable"
14
14
 
15
15
  ActiveSupport.on_load(:active_record) do
16
- include ActiveKit::Base::Activekitable
16
+ include ActiveKit::Position::Positionable
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,71 @@
1
+ module ActiveKit
2
+ module Position
3
+ class Harmonize
4
+ def initialize(current_class:, name:, scope:)
5
+ @current_class = current_class
6
+ @name = name
7
+
8
+ @scoped_class = @current_class.where(scope)
9
+ @positioning = Positioning.new
10
+ @batch_size = 1000
11
+ end
12
+
13
+ def run!
14
+ @current_class.transaction do
15
+ chair_at_params, scoped_class_with_order, chair_method, offset_operator = control
16
+ currvalue = @positioning.chair_at(**chair_at_params, increase_spot_length_by: 1).first
17
+
18
+ first_run = true
19
+ where_offset = nil
20
+ loop do
21
+ records = scoped_class_with_order.where(where_offset).limit(@batch_size)
22
+ break if records.empty?
23
+
24
+ records.lock.each do |record|
25
+ value, reharmonize = first_run ? [currvalue, false] : @positioning.public_send(chair_method, currvalue: currvalue)
26
+ raise message_for_reharmonize if reharmonize
27
+
28
+ record.send("#{@name}=", value)
29
+ record.save!
30
+
31
+ currvalue = record.public_send("#{@name}")
32
+ first_run = false
33
+ end
34
+
35
+ where_offset = ["#{@name} #{offset_operator} ?", currvalue]
36
+ end
37
+ end
38
+
39
+ Rails.logger.info "ActiveKit::Position | Harmonize for :#{@name}: completed."
40
+ end
41
+
42
+ private
43
+
44
+ def control
45
+ records = @scoped_class.where.not("#{@name}": nil).order("#{@name}": :asc, id: :asc).select(@name.to_sym)
46
+ headtier = records.first&.try(@name.to_sym)&.split("|")&.first&.last&.to_i # returns a tire integer
47
+ foottier = records.last&.try(@name.to_sym)&.split("|")&.first&.last&.to_i # returns a tire integer
48
+
49
+ if headtier == foottier
50
+ nexttier, ordering = (headtier == 0) ? [1, :foot_to_head] : [0, :head_to_foot]
51
+ else
52
+ maxitier = (headtier.nil? || foottier.nil?) ? (headtier.nil? ? foottier : headtier) : (headtier > foottier ? headtier : foottier)
53
+ nexttier, ordering = (maxitier + 1), :foot_to_head
54
+ end
55
+ scoped_order, chair_method, offset_operator = (ordering == :head_to_foot) ? [:asc, :chair_below, ">"] : [:desc, :chair_above, "<"]
56
+
57
+ scoped_class_with_order = @scoped_class.order("#{@name}": scoped_order, id: scoped_order)
58
+ scoped_class_with_order_count = scoped_class_with_order.count
59
+ initial_position = (ordering == :head_to_foot) ? 1 : scoped_class_with_order_count
60
+
61
+ chair_at_params = { position: initial_position, tier_no: nexttier, total_count: scoped_class_with_order_count }
62
+
63
+ [chair_at_params, scoped_class_with_order, chair_method, offset_operator]
64
+ end
65
+
66
+ def message_for_reharmonize
67
+ "Harmonize cannot ask to harmonize again. Please check values of attribute '#{@name}' and try again."
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,22 +1,23 @@
1
1
  module ActiveKit
2
- module Base
2
+ module Position
3
3
  class Middleware
4
4
  def initialize(app)
5
5
  @app = app
6
6
  end
7
7
 
8
- # Middleware that determines which ActiveKit middlewares to run.
9
8
  def call(env)
10
9
  request = ActionDispatch::Request.new(env)
11
10
 
12
- activekit_run(request) do
11
+ middleware_run(request) do
13
12
  @app.call(env)
14
13
  end
15
14
  end
16
15
 
17
16
  private
18
17
 
19
- def activekit_run(request, &blk)
18
+ def middleware_run(request, &blk)
19
+ # Position middleware code
20
+
20
21
  yield
21
22
  end
22
23
  end
@@ -0,0 +1,77 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveKit
4
+ module Position
5
+ module Positionable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ end
10
+
11
+ class_methods do
12
+ def position_attribute(name, **options)
13
+ scope = options[:scope] || {}
14
+
15
+ attribute "#{name}_position", :integer
16
+
17
+ validates "#{name}", presence: true, uniqueness: { conditions: -> { where(scope) }, case_sensitive: false, allow_blank: true }, length: { maximum: 255, allow_blank: true }
18
+ validates "#{name}_position", numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: lambda { |record| record.public_send("#{name}_position_maximum") + 1 }, allow_blank: true }
19
+
20
+ before_validation "#{name}_reposition".to_sym
21
+ after_commit "#{name}_reharmonize".to_sym
22
+
23
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
24
+ def #{name}_position_in_database
25
+ self.#{name}_positioner.position_in_database
26
+ end
27
+
28
+ def #{name}_position_options
29
+ self.#{name}_positioner.position_options
30
+ end
31
+
32
+ def #{name}_position_maximum
33
+ self.#{name}_positioner.position_maximum
34
+ end
35
+
36
+ def self.harmonize_#{name}!
37
+ ActiveKit::Position::Harmonize.new(current_class: self, name: '#{name}', scope: #{scope}).run!
38
+ end
39
+
40
+ private
41
+
42
+ def #{name}=(value)
43
+ super(value)
44
+ end
45
+
46
+ def #{name}_positioner
47
+ @#{name}_positioner ||= ActiveKit::Position::Positioner.new(record: self, name: '#{name}', scope: #{scope})
48
+ end
49
+
50
+ def #{name}_reposition
51
+ return unless (self.#{name}.blank? && self.#{name}_position.blank?) || self.#{name}_position.present?
52
+
53
+ position_maximum_cached = self.#{name}_positioner.position_maximum
54
+
55
+ if self.#{name}.blank? && self.#{name}_position.blank?
56
+ self.#{name}_position = position_maximum_cached + 1
57
+ end
58
+
59
+ if self.#{name}_position.present? && self.#{name}_position >= 1 && self.#{name}_position <= (position_maximum_cached + 1)
60
+ if self.#{name}_position != #{name}_position_in_database
61
+ self.#{name} = self.#{name}_positioner.spot_for(position: self.#{name}_position, position_maximum_cached: position_maximum_cached)
62
+ end
63
+ end
64
+ end
65
+
66
+ def #{name}_reharmonize
67
+ return unless self.#{name}_positioner.reharmonize?
68
+
69
+ self.class.harmonize_#{name}!
70
+ self.#{name}_positioner.reharmonized!
71
+ end
72
+ CODE
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,87 @@
1
+ module ActiveKit
2
+ module Position
3
+ class Positioner
4
+ def initialize(record:, name:, scope:)
5
+ @record = record
6
+ @name = name
7
+
8
+ @scoped_class = @record.class.where(scope).order("#{@name}": :asc)
9
+ @reharmonize = false
10
+ @positioning = Positioning.new
11
+ end
12
+
13
+ def position_in_database
14
+ @scoped_class.where("#{@name}": ..@record.public_send("#{@name}_in_database")).count if @record.public_send("#{@name}_in_database")
15
+ end
16
+
17
+ def position_options
18
+ (1..position_maximum).map { |position| [position, "Position #{position}"] }
19
+ end
20
+
21
+ def position_maximum
22
+ value = self.maxivalue
23
+ value ? @scoped_class.where("#{@name}": ..value).count : 0
24
+ end
25
+
26
+ def spot_for(position:, position_maximum_cached:)
27
+ raise "position or position_maximum_cached cannot be empty for spot_for in activekit." unless position && position_maximum_cached
28
+
29
+ edge_position = position_maximum_cached + 1
30
+ if position == edge_position && position == 1
31
+ value, @reharmonize = @positioning.chair_first
32
+ elsif position == edge_position
33
+ value, @reharmonize = @positioning.chair_below(currvalue: self.maxivalue)
34
+ elsif position_in_database.nil?
35
+ value, @reharmonize = @positioning.stool_above(currvalue: currvalue(position, position_maximum_cached),
36
+ prevvalue: prevvalue(position, position_maximum_cached))
37
+ elsif position > position_in_database
38
+ value, @reharmonize = @positioning.stool_below(currvalue: currvalue(position, position_maximum_cached),
39
+ nextvalue: nextvalue(position, position_maximum_cached))
40
+ else
41
+ value, @reharmonize = @positioning.stool_above(currvalue: currvalue(position, position_maximum_cached),
42
+ prevvalue: prevvalue(position, position_maximum_cached))
43
+ end
44
+
45
+ value
46
+ end
47
+
48
+ def reharmonize?
49
+ @reharmonize
50
+ end
51
+
52
+ def reharmonized!
53
+ @reharmonize = false
54
+ end
55
+
56
+ private
57
+
58
+ def maxivalue
59
+ @scoped_class.last&.public_send(@name)
60
+ end
61
+
62
+ def prevvalue(position, position_maximum_cached)
63
+ prevvalue?(position, position_maximum_cached) ? @scoped_class.offset(position - 2).first.public_send(@name) : nil
64
+ end
65
+
66
+ def currvalue(position, position_maximum_cached)
67
+ currvalue?(position, position_maximum_cached) ? @scoped_class.offset(position - 1).first.public_send(@name) : nil
68
+ end
69
+
70
+ def nextvalue(position, position_maximum_cached)
71
+ nextvalue?(position, position_maximum_cached) ? @scoped_class.offset(position - 0).first.public_send(@name) : nil
72
+ end
73
+
74
+ def prevvalue?(position, position_maximum_cached)
75
+ position > 1
76
+ end
77
+
78
+ def currvalue?(position, position_maximum_cached)
79
+ position >= 1 && position <= position_maximum_cached
80
+ end
81
+
82
+ def nextvalue?(position, position_maximum_cached)
83
+ position < position_maximum_cached
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,86 @@
1
+ module ActiveKit
2
+ module Position
3
+ class Positioning
4
+ BASE = 36
5
+
6
+ def initialize
7
+ @initial_tier = "t0"
8
+ @initial_spot = "00"
9
+ @initial_slot = "hz"
10
+ end
11
+
12
+ def chair_first
13
+ chair(@initial_tier, @initial_spot, @initial_slot)
14
+ end
15
+
16
+ def chair_at(position:, tier_no:, total_count:, increase_spot_length_by:)
17
+ nicetire = "t#{tier_no}"
18
+ nicespot = (position - 1).to_s(BASE)
19
+ nicespot = nicespot.rjust((total_count - 1).to_s(BASE).length + increase_spot_length_by, "0")
20
+ chair(nicetire, nicespot, @initial_slot)
21
+ end
22
+
23
+ def chair_above(currvalue:)
24
+ currtier, currspot = currvalue.split("|").take(2)
25
+ nicespot = (currspot.to_i(BASE) - 1).to_s(BASE).rjust(currspot.length, "0")
26
+ nicespot = firstspot(currspot) if nicespot.to_i(BASE) == 0
27
+ chair(currtier, nicespot, @initial_slot)
28
+ end
29
+
30
+ def chair_below(currvalue:)
31
+ currtier, currspot = currvalue.split("|").take(2)
32
+ nicespot = (currspot.to_i(BASE) + 1).to_s(BASE).rjust(currspot.length, "0")
33
+ nicespot = finalspot(currspot) if nicespot.to_i(BASE) > finalspot(currspot).to_i(BASE)
34
+ chair(currtier, nicespot, @initial_slot)
35
+ end
36
+
37
+ def stool_above(currvalue:, prevvalue:)
38
+ currtier, currspot, currslot = currvalue.split("|")
39
+ prevtier, prevspot, prevslot = prevvalue.split("|") if prevvalue
40
+ stool(currtier, currspot, firstslot(currtier, prevtier, currspot, prevspot, prevslot), currslot)
41
+ end
42
+
43
+ def stool_below(currvalue:, nextvalue:)
44
+ currtier, currspot, currslot = currvalue.split("|")
45
+ nexttier, nextspot, nextslot = nextvalue.split("|") if nextvalue
46
+ stool(currtier, currspot, currslot, finalslot(currtier, nexttier, currspot, nextspot, nextslot))
47
+ end
48
+
49
+ private
50
+
51
+ def firstspot(currspot)
52
+ "0".rjust(currspot.length, "0")
53
+ end
54
+
55
+ def finalspot(currspot)
56
+ "z".rjust(currspot.length, "z")
57
+ end
58
+
59
+ def firstslot(currtier, prevtier, currspot, prevspot, prevslot)
60
+ currtier == prevtier && currspot == prevspot ? prevslot : "00"
61
+ end
62
+
63
+ def finalslot(currtier, nexttier, currspot, nextspot, nextslot)
64
+ currtier == nexttier && currspot == nextspot ? nextslot : "zz"
65
+ end
66
+
67
+ def chair(tier, spot, slot)
68
+ ["#{tier}|#{spot}|#{slot}", chairs_almost_over?(spot)]
69
+ end
70
+
71
+ def stool(tier, spot, slot_small, slot_big)
72
+ avg_slot = ((slot_big.to_i(BASE) + slot_small.to_i(BASE)) / 2).to_s(BASE).rjust(slot_big.length, "0")
73
+ ["#{tier}|#{spot}|#{avg_slot}", stools_almost_over?(slot_small, slot_big)]
74
+ end
75
+
76
+ def chairs_almost_over?(spot)
77
+ finalspot(spot).to_i(BASE) - spot.to_i(BASE) <= 10
78
+ end
79
+
80
+ # Careful, when 'true' stools are much less than 10 because of division by 2 to get the next stool.
81
+ def stools_almost_over?(slot_small, slot_big)
82
+ (slot_big.to_i(BASE) - slot_small.to_i(BASE)) <= 10
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveKit
2
+ module Position
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Harmonize
6
+ autoload :Positioner
7
+ autoload :Positioning
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.2.3'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/active_kit.rb CHANGED
@@ -4,6 +4,5 @@ require "active_kit/engine"
4
4
  module ActiveKit
5
5
  extend ActiveSupport::Autoload
6
6
 
7
- autoload :Base
8
- autoload :Sequence
7
+ autoload :Position
9
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activekit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - plainsource
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-06 00:00:00.000000000 Z
11
+ date: 2023-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -47,22 +47,16 @@ files:
47
47
  - app/jobs/active_kit/application_job.rb
48
48
  - app/mailers/active_kit/application_mailer.rb
49
49
  - app/models/active_kit/application_record.rb
50
- - app/models/active_kit/attribute.rb
51
50
  - app/views/layouts/active_kit/application.html.erb
52
51
  - config/routes.rb
53
- - db/migrate/20231016050208_create_active_kit_attributes.rb
54
52
  - lib/active_kit.rb
55
- - lib/active_kit/base.rb
56
- - lib/active_kit/base/activekitable.rb
57
- - lib/active_kit/base/activekiter.rb
58
- - lib/active_kit/base/ensure.rb
59
- - lib/active_kit/base/middleware.rb
60
- - lib/active_kit/base/relation.rb
61
53
  - lib/active_kit/engine.rb
62
- - lib/active_kit/sequence.rb
63
- - lib/active_kit/sequence/sequence.rb
64
- - lib/active_kit/sequence/sequenceable.rb
65
- - lib/active_kit/sequence/wordbook.rb
54
+ - lib/active_kit/position.rb
55
+ - lib/active_kit/position/harmonize.rb
56
+ - lib/active_kit/position/middleware.rb
57
+ - lib/active_kit/position/positionable.rb
58
+ - lib/active_kit/position/positioner.rb
59
+ - lib/active_kit/position/positioning.rb
66
60
  - lib/active_kit/version.rb
67
61
  - lib/activekit.rb
68
62
  - lib/tasks/active_kit_tasks.rake
@@ -1,17 +0,0 @@
1
- module ActiveKit
2
- class Attribute < ApplicationRecord
3
- belongs_to :record, polymorphic: true
4
-
5
- store :value, accessors: [ :sequence ], coder: JSON
6
-
7
- validates :value, presence: true, length: { maximum: 1073741823, allow_blank: true }
8
-
9
- before_validation :set_defaults
10
-
11
- private
12
-
13
- def set_defaults
14
- self.sequence = { attributes: {} } if self.sequence.blank?
15
- end
16
- end
17
- end
@@ -1,9 +0,0 @@
1
- class CreateActiveKitAttributes < ActiveRecord::Migration[6.1]
2
- def change
3
- create_table :active_kit_attributes do |t|
4
- t.references :record, polymorphic: true, index: true
5
- t.text :value, size: :long
6
- t.timestamps
7
- end
8
- end
9
- end
@@ -1,17 +0,0 @@
1
- require 'active_support/concern'
2
- require "active_kit/sequence/sequenceable"
3
-
4
- module ActiveKit
5
- module Base
6
- module Activekitable
7
- extend ActiveSupport::Concern
8
- include ActiveKit::Sequence::Sequenceable
9
-
10
- included do
11
- end
12
-
13
- class_methods do
14
- end
15
- end
16
- end
17
- end
@@ -1,13 +0,0 @@
1
- module ActiveKit
2
- module Base
3
- class Activekiter
4
- def initialize(current_class:)
5
- @current_class = current_class
6
- end
7
-
8
- def sequence
9
- @sequence ||= ActiveKit::Sequence::Sequence.new(current_class: @current_class)
10
- end
11
- end
12
- end
13
- end
@@ -1,25 +0,0 @@
1
- module ActiveKit
2
- module Base
3
- class Ensure
4
- def self.setup_for!(current_class:)
5
- current_class.class_eval do
6
- unless self.reflect_on_association :activekit_association
7
- has_one :activekit_association, as: :record, dependent: :destroy, class_name: "ActiveKit::Attribute"
8
-
9
- def activekit
10
- @activekit ||= Relation.new(current_object: self)
11
- end
12
-
13
- def self.activekiter
14
- @activekiter ||= Activekiter.new(current_class: self)
15
- end
16
- end
17
- end
18
- end
19
-
20
- def self.has_one_association_for!(record:)
21
- record.create_activekit_association unless record.activekit_association
22
- end
23
- end
24
- end
25
- end
@@ -1,9 +0,0 @@
1
- module ActiveKit
2
- module Base
3
- class Relation
4
- def initialize(current_object:)
5
- @current_object = current_object
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module ActiveKit
2
- module Base
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :Activekiter
6
- autoload :Ensure
7
- autoload :Relation
8
- end
9
- end
@@ -1,38 +0,0 @@
1
- module ActiveKit
2
- module Sequence
3
- class Sequence
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
- ActiveKit::Base::Ensure.has_one_association_for!(record: record)
15
-
16
- if position
17
- raise "position '#{position}' is not a valid unsigned integer value greater than 0." unless position.is_a?(Integer) && position > 0
18
-
19
- wordbook = Wordbook.new
20
- word_for_position = wordbook.next_word(count: position)
21
- # TODO: committer record for the attribute with given word_for_position should be found and resaved to recalculate its position.
22
- # json_where = 'value->"$.sequence.attributes.' + attribute_name.to_s + '" = "' + word_for_position + '"'
23
- # record_at_position = ActiveKit::Attribute.where(record_type: record.class.name).where(json_where).first&.record
24
- record.activekit_association.sequence[:attributes][attribute_name.to_sym] = word_for_position
25
- record.activekit_association.save!
26
- # record_at_position.save! if record_at_position
27
- else
28
- record.activekit_association.sequence[:attributes][attribute_name.to_sym] = nil
29
- record.activekit_association.save!
30
- end
31
- end
32
-
33
- def add_attribute(name:, options:)
34
- @defined_attributes.store(name, options)
35
- end
36
- end
37
- end
38
- end
@@ -1,98 +0,0 @@
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 = nil, **options)
21
- ActiveKit::Base::Ensure.setup_for!(current_class: self)
22
-
23
- name = name.to_sym
24
- options.store(:positioning_method, positioning_method&.to_sym)
25
- options.deep_symbolize_keys!
26
-
27
- set_active_sequence_callbacks(attribute_name: name, options: options)
28
- activekiter.sequence.add_attribute(name: name, options: options)
29
- end
30
-
31
- def set_active_sequence_callbacks(attribute_name:, options:)
32
- positioning_method = options.dig(:positioning_method)
33
- updater = options.dig(:updater) || {}
34
-
35
- if updater.empty?
36
- after_save do
37
- position = positioning_method ? self.public_send(positioning_method) : nil
38
- self.class.activekiter.sequence.update(record: self, attribute_name: attribute_name, position: position)
39
- logger.info "ActiveSequence - Sequencing from #{self.class.name}: Done."
40
- end
41
- else
42
- raise ":updater should be a hash while setting sequence_attribute. " unless updater.is_a?(Hash)
43
- raise ":on in :updater should be a hash while setting sequence_attribute. " if updater.key?(:on) && !updater[:on].is_a?(Hash)
44
- raise "Cannot use :via without :on in :updater while setting sequence_attribute. " if updater.key?(:via) && !updater.key?(:on)
45
-
46
- updater_via = updater.delete(:via)
47
- updater_on = updater.delete(:on) || updater
48
-
49
- base_klass = search_base_klass(self.name, updater_via)
50
- klass = reflected_klass(base_klass, updater_on.keys.first)
51
- klass.constantize.class_eval do
52
- after_save :activekit_sequence_sequenceable_callback
53
- after_destroy :activekit_sequence_sequenceable_callback
54
-
55
- define_method :activekit_sequence_sequenceable_callback do
56
- inverse_assoc = self.class.search_inverse_assoc(self, updater_on)
57
- position = positioning_method ? self.public_send(positioning_method) : nil
58
- if inverse_assoc.respond_to?(:each)
59
- inverse_assoc.each { |instance| instance.class.activekiter.sequence.update(record: instance, attribute_name: attribute_name, position: position) }
60
- else
61
- inverse_assoc.class.activekiter.sequence.update(record: inverse_assoc, attribute_name: attribute_name, position: position)
62
- end
63
- logger.info "ActiveSequence - Sequencing from #{self.class.name}: Done."
64
- end
65
- private :activekit_sequence_sequenceable_callback
66
- end
67
- end
68
- end
69
-
70
- def search_base_klass(classname, updater_via)
71
- if updater_via.blank?
72
- classname
73
- elsif updater_via.is_a? Symbol
74
- reflected_klass(classname, updater_via)
75
- elsif updater_via.is_a? Hash
76
- klass = reflected_klass(classname, updater_via.keys.first)
77
- updater_via.values.first.is_a?(Hash) ? search_base_klass(klass, updater_via.values.first) : reflected_klass(klass, updater_via.values.first)
78
- end
79
- end
80
-
81
- def reflected_klass(classname, key)
82
- klass = classname.constantize.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[key]
83
- raise "Could not find reflected klass for classname '#{classname}' and key '#{key}' while setting sequence_attribute" unless klass
84
- klass
85
- end
86
-
87
- def search_inverse_assoc(klass_object, updater_on)
88
- if updater_on.values.first.is_a?(Hash)
89
- klass_object = klass_object.public_send(updater_on.values.first.keys.first)
90
- search_inverse_assoc(klass_object, updater_on.values.first)
91
- else
92
- klass_object.public_send(updater_on.values.first)
93
- end
94
- end
95
- end
96
- end
97
- end
98
- end
@@ -1,60 +0,0 @@
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
@@ -1,8 +0,0 @@
1
- module ActiveKit
2
- module Sequence
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :Sequence
6
- autoload :Wordbook
7
- end
8
- end