garland 0.1

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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/garland.rb +1 -0
  3. data/lib/garland_rails.rb +216 -0
  4. metadata +60 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fcb60a9b283bda379d5796264e73b3061cb48a56
4
+ data.tar.gz: bb24cf3f58282f101eb2b5b02536d9ce0e6c54b0
5
+ SHA512:
6
+ metadata.gz: 6a5dd458eee1f2a1fadb994d43fd9c2583316e8fde7c212981297d95ebba86c774beb26cdd8a65533c8f2eb198eabb239fb4052dbb217a81015e98332727f5a0
7
+ data.tar.gz: be060085315be33b9355df2910959aa5dd0addba59a166933671ab5cbd977fa59ea105b1883fb9566e0015fe798d67eb8d6b00840b7386c2834c156db64e5bb6
data/lib/garland.rb ADDED
@@ -0,0 +1 @@
1
+ Garland = Class.new(ActiveRecord::Base)
@@ -0,0 +1,216 @@
1
+ module GarlandRails
2
+ DIFF = true
3
+ SNAPSHOT = false
4
+
5
+ class Base < Garland
6
+ self.table_name = "garlands"
7
+
8
+ validates :entity, presence: true
9
+ validates_inclusion_of :entity_type, in: [DIFF, SNAPSHOT]
10
+
11
+ # for given parent object, described by "belongs_to_id" and "belongs_to_type",
12
+ # there are only one record of each type which is head or tail
13
+ validates_uniqueness_of :type, scope: [
14
+ :belongs_to_id,
15
+ :belongs_to_type,
16
+ :next,
17
+ ], conditions: -> { where("next IS NULL OR previous IS NULL") }
18
+
19
+ def self.belongs_to(type)
20
+ super type, foreign_key: "belongs_to_id"
21
+ validates type, presence: true
22
+ validates_inclusion_of :belongs_to_type, in: ["#{type.capitalize}"]
23
+ end
24
+
25
+ def self.push(args)
26
+ return nil unless args.class == Hash
27
+
28
+ if args[:hash]
29
+ hash = args[:hash]
30
+ return nil unless hash.class == Hash
31
+
32
+ belongs_to = args[:belongs_to]
33
+ if belongs_to
34
+ belongs_to_params = self._split_belongs_to(belongs_to)
35
+ belongs_to_id = belongs_to_params[:belongs_to_id]
36
+ belongs_to_type = belongs_to_params[:belongs_to_type]
37
+ end
38
+ else
39
+ hash = args
40
+ belongs_to = nil
41
+ belongs_to_id = nil
42
+ belongs_to_type = nil
43
+ end
44
+
45
+ head = self.head(belongs_to)
46
+ if head
47
+ diff = self.insert_diff(hash, belongs_to)
48
+ else
49
+ diff = self.init(hash, belongs_to)
50
+ end
51
+
52
+ diff
53
+ end
54
+
55
+ def self.thread(belongs_to = nil)
56
+ if belongs_to
57
+ return self.where(
58
+ "belongs_to_id = ? AND belongs_to_type = ?",
59
+ belongs_to.id, table_type(belongs_to)
60
+ )
61
+ else
62
+ return self.where("belongs_to_id is null AND belongs_to_type is null")
63
+ end
64
+ end
65
+
66
+ def self.tail(belongs_to = nil)
67
+ self.thread(belongs_to).find_by(previous: nil)
68
+ end
69
+
70
+ def self.head(belongs_to = nil)
71
+ self.thread(belongs_to).find_by(next: nil)
72
+ end
73
+
74
+ def self.last_diff(belongs_to = nil)
75
+ head = self.head(belongs_to)
76
+
77
+ self.find_by(id: head.previous)
78
+ end
79
+
80
+ def self.table_type(record)
81
+ record.class.name
82
+ end
83
+
84
+ def self.any?(belongs_to = nil)
85
+ self.thread(belongs_to).any?
86
+ end
87
+
88
+ def self.init(hash, belongs_to = nil)
89
+ common_props = self._split_belongs_to(belongs_to)
90
+
91
+ tail_props = common_props.merge({ entity: {}.to_s, entity_type: SNAPSHOT })
92
+ brand_new_tail = self.new(tail_props)
93
+
94
+ diff = HashDiffSym.diff({}, hash)
95
+ first_diff_props = common_props.merge({ entity: diff.to_s, entity_type: DIFF })
96
+ first_diff = self.new(first_diff_props)
97
+
98
+ head_props = common_props.merge({ entity: hash.to_s, entity_type: SNAPSHOT })
99
+ brand_new_head = self.new(head_props)
100
+
101
+ self.transaction do
102
+ ActiveRecord::Base.connection.create_savepoint("savepoint_before_init")
103
+
104
+ # first id: tail ({})
105
+ # second id: head (latest snapshot)
106
+ # third+: diffs
107
+ unless brand_new_tail.save
108
+ Rails.logger.error("Unable to create new tail with props '#{tail_props}'")
109
+ return nil
110
+ end
111
+
112
+ # belongs_to validations were in `brand_new_tail.save`
113
+ # here and below validations may be skipped as long as we check for continuity later
114
+ first_diff.save(validate: false)
115
+ brand_new_head.save(validate: false)
116
+ brand_new_tail.update_attribute(:next, first_diff.id)
117
+ first_diff.update_attribute(:previous, brand_new_tail.id)
118
+ first_diff.update_attribute(:next, brand_new_head.id)
119
+ brand_new_head.update_attribute(:previous, first_diff.id)
120
+
121
+ unless self.continuous?(belongs_to)
122
+ Rails.logger.error("Initialized garland is not continuous")
123
+ ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_init")
124
+ return nil
125
+ end
126
+ end
127
+
128
+ first_diff
129
+ end
130
+
131
+ def self.insert_diff(hash, belongs_to = nil)
132
+ head = self.head(belongs_to)
133
+ last_diff = self.find_by(id: head.previous)
134
+ common_props = self._split_belongs_to(belongs_to)
135
+
136
+ diff = HashDiffSym.diff(eval(head.entity), hash)
137
+ return unless diff.any?
138
+
139
+ new_diff_props = common_props.merge({
140
+ entity: diff.to_s,
141
+ entity_type: DIFF,
142
+ previous: last_diff.id,
143
+ next: head.id,
144
+ })
145
+ new_diff = self.new(new_diff_props)
146
+
147
+ self.transaction do
148
+ ActiveRecord::Base.connection.create_savepoint("savepoint_before_insert_diff")
149
+
150
+ # insert_diff should not use skipping valudatuons methods
151
+ # because we don't want to check for continuity on every push
152
+ unless new_diff.save
153
+ Rails.logger.error("Unable to create new_diff with props '#{new_diff_props}'")
154
+ return nil
155
+ end
156
+
157
+ last_diff.next = new_diff.id
158
+ unless last_diff.save
159
+ Rails.logger.error("Unable to save last_diff with 'next' = '#{new_diff.id}'")
160
+ ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
161
+ return nil
162
+ end
163
+
164
+ head.previous = new_diff.id
165
+ unless head.save
166
+ Rails.logger.error("Unable to save head with 'previous' = '#{new_diff.id}'")
167
+ ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
168
+ return nil
169
+ end
170
+
171
+ head.entity = hash.to_s
172
+ unless head.save
173
+ Rails.logger.error("Unable to save head with 'entity' = '#{hash.to_s}'")
174
+ ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
175
+ return nil
176
+ end
177
+ end
178
+
179
+ new_diff
180
+ end
181
+
182
+ def self.continuous?(belongs_to = nil)
183
+ tail = self.tail(belongs_to)
184
+ head = self.head(belongs_to)
185
+ return false unless tail && head
186
+
187
+ current_bulb = tail
188
+ current_hash = eval(tail.entity)
189
+ items_counted = 1
190
+ while current_bulb.next do
191
+ items_counted += 1
192
+ current_bulb = self.find_by(id: current_bulb.next)
193
+ if current_bulb.entity_type == DIFF
194
+ current_hash = HashDiffSym.patch!(current_hash, eval(current_bulb.entity))
195
+ else
196
+ break
197
+ end
198
+ end
199
+
200
+ items_counted == self.thread(belongs_to).size && current_hash == eval(head.entity)
201
+ end
202
+
203
+ private
204
+ def self._split_belongs_to(belongs_to)
205
+ if belongs_to
206
+ belongs_to_id = belongs_to.id
207
+ belongs_to_type = table_type(belongs_to)
208
+ else
209
+ belongs_to_id = nil
210
+ belongs_to_type = nil
211
+ end
212
+
213
+ { belongs_to_id: belongs_to_id, belongs_to_type: belongs_to_type }
214
+ end
215
+ end
216
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: garland
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Morozov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashdiff_sym
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
+ description: Provides GarlandRails::Base class for ActiveRecord, which allows to save
28
+ Hashes using snapshots and diffs.
29
+ email: ntcomp12@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/garland.rb
35
+ - lib/garland_rails.rb
36
+ homepage: https://github.com/kengho/garland
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 2.5.2
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: HashDiff ActiveRecord storage
60
+ test_files: []