garland 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []