activekit 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 630c8d871f28fedaf525eecbf06fdfb396bba0f62282ec3a8d7d9ef359a833c4
4
- data.tar.gz: 154de328d7cc8e4520ff9f5f5f57ee6b1e73cf0df352d2aaee97387dca9a06a1
3
+ metadata.gz: c73cb76ead023d8565b199c3fc7a287108c058423c558a15382f150de64d549a
4
+ data.tar.gz: a1772c9e23aba4cc64bd21756c09770fa386d241f43d78ef346d3c94cd6c823d
5
5
  SHA512:
6
- metadata.gz: 68aaf23851ac2ba3ae9d73eb4474a3c7fe789c7e6f6e61c0d0e85d23a77f159c9e219cd635a28f99cc215f0b4605e43046196ebbf384f172e4ac0bd0f33fce95
7
- data.tar.gz: 0753d639192b574bda48eee23df69d8f23e95d24c6c03f7571b4fb87c28a99a08bfd346a30b7ff0b0a271a890d4878e2cd37c7aa50bf912d0f21d29014e968f1
6
+ metadata.gz: 43e7e2a9318cb06bd2580d5d8a1a7ce34336de43bf1f66afed0b447e66daecc634713864483a4ff48f113392d5f160afe677600f2b5e83f473204603b7595d78
7
+ data.tar.gz: 8f73c668f085980288cfbea6743a83d5999807789160cdbcbd01cb345f39cba386d9d80ec2d2af344a8e3a05a3e37ed0d2ce5642a77d7c67c1bed62ec07ecbb6
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,47 @@
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 having an index.
14
+ ```ruby
15
+ add_column :products, :arrangement, :string
16
+ add_index :products, :arrangement
17
+ ```
18
+
19
+ Then define the column name in your model like below.
20
+ ```ruby
21
+ class Product < ApplicationRecord
22
+ position_attribute :arrangement
23
+ end
24
+ ```
25
+
26
+ Creating the record will automatically set it to the last position:
27
+ ```ruby
28
+ product = Product.create(name: "Nice Product")
29
+ ```
30
+
31
+ The following attribute methods will be added to your model object to use:
32
+ ```ruby
33
+ product.arrangement_position = 1 # Set the position
34
+ product.arrangement_position # Access the manually set position
35
+ product.arrangement_position_in_database # Check the position as per database
36
+ product.arrangement_position_options # Can be used in dropdown
37
+ product.arrangement_position_maximum # Check current maximum position
38
+ ```
39
+
40
+ The following class methods will be added to your model class to use:
41
+ ```ruby
42
+ # If a database table already has existing rows, run this to set initial values.
43
+ # You can also run this to manually harmonize a position attribute column.
44
+ Product.harmonize_arrangement!
45
+ ```
6
46
 
7
47
  ## Installation
8
48
  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).reorder("#{@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.reorder("#{@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.1'
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.1
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: 2024-01-01 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