snappier 0.1.0 → 0.1.2

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: 2927f513086b61df8edaadcc710dfdfde6bce30f94710c3d78a343ddce5b1161
4
- data.tar.gz: 825ad55fc4328d3168d7c37fbd4e85952546c9d0c77281b466f93e0e569e44ea
3
+ metadata.gz: b2b91bfac18970f816c688c50f628f2936b4d9542d91cf2fb941bed3e89d20c3
4
+ data.tar.gz: 9852bc0fc596ee1f8c5e38a56334bdb5c95fae7366325443dc66fd4dfa625053
5
5
  SHA512:
6
- metadata.gz: e8a9a9c560b514a332ed813a53c1d9f9e9845ed44cbdbe7eb0bd36f0e5c9f4ffd834cc5d0f236a5b914f0d48a2d385e5f17ff06808b6d3e8dffa37f12f6ed30a
7
- data.tar.gz: fdff950af91eaa75ebfd44b02ecc3d25ebf58541fd10adb92cf57d6856ae0d3fae5318315b268f72d2e1d9127025146fa5240c889e8842c22796ae49368765a3
6
+ metadata.gz: c3735760364dd88d933d8ad919f6c886d27f8a98d400915b649e7aa902ba3392f7f554ee5beac7e000c8540d6049aba39047e9cfe447c83e76ad8c2681ae781e
7
+ data.tar.gz: 3eef0ad2728221c2390d1770dcd1505ca8650f0b10042f9d8d24f367da9fba82557e9f2b8fadf045708a8df8eb3b07afe3c7a909c7a052ec99afb02510f8d3b0
data/.rubocop.yml CHANGED
@@ -1,8 +1,43 @@
1
+ plugins:
2
+ - rubocop-rake
3
+ - rubocop-rspec
4
+
1
5
  AllCops:
2
6
  TargetRubyVersion: 3.1
7
+ NewCops: enable
3
8
 
4
9
  Style/StringLiterals:
5
10
  EnforcedStyle: double_quotes
6
11
 
7
12
  Style/StringLiteralsInInterpolation:
8
13
  EnforcedStyle: double_quotes
14
+
15
+ Style/TrailingCommaInArguments:
16
+ EnforcedStyleForMultiline: comma
17
+
18
+ Style/Documentation:
19
+ Enabled: false
20
+
21
+ RSpec/ExampleLength:
22
+ Enabled: false
23
+
24
+ RSpec/InstanceVariable:
25
+ Enabled: false
26
+
27
+ RSpec/MultipleExpectations:
28
+ Enabled: false
29
+
30
+ RSpec/VerifiedDoubles:
31
+ Enabled: false
32
+
33
+ Metrics/AbcSize:
34
+ Max: 30
35
+
36
+ Metrics/CyclomaticComplexity:
37
+ Max: 15
38
+
39
+ Metrics/MethodLength:
40
+ Max: 30
41
+
42
+ Metrics/PerceivedComplexity:
43
+ Max: 15
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), "..", "lib")
5
+
6
+ require "snappier"
7
+
8
+ type = ARGV.shift
9
+ id = ARGV.shift
10
+
11
+ Snappier::Replay.for(
12
+ type: type,
13
+ id: id,
14
+ ) do |entry|
15
+ pp entry
16
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Snappier
6
+ module Changes
7
+ def self.between(previous_state, current_state)
8
+ {}.tap { |changes| append_changes(changes, previous_state, current_state) }
9
+ end
10
+
11
+ def self.append_changes(changes, previous_state, current_state, path = [])
12
+ keys = Set.new(previous_state.keys + current_state.keys)
13
+
14
+ keys.each do |key|
15
+ previous_value = previous_state[key]
16
+ current_value = current_state[key]
17
+
18
+ if previous_value.is_a?(Array) || current_value.is_a?(Array)
19
+ append_changes_for_collections(changes, previous_value, current_value, path + [key])
20
+ next
21
+ end
22
+
23
+ if previous_value.is_a?(Hash) || current_value.is_a?(Hash)
24
+ previous_id = (previous_value || {}).delete("id")
25
+ current_id = (current_value || {}).delete("id")
26
+
27
+ if previous_id || current_id
28
+ if previous_id == current_id
29
+ append_changes(changes, previous_value, current_value, path + [key, previous_id])
30
+ else
31
+ append_changes(changes, previous_value, {}, path + [key, previous_id])
32
+ append_changes(changes, {}, current_value, path + [key, current_id])
33
+ end
34
+ else
35
+ append_changes(changes, previous_value, current_value, path + [key])
36
+ end
37
+
38
+ next
39
+ end
40
+
41
+ next if previous_value == current_value
42
+
43
+ changes[path + [key]] = [previous_value, current_value]
44
+ end
45
+ end
46
+
47
+ def self.append_changes_for_collections(changes, previous_collection, current_collection, path)
48
+ ids = Set.new
49
+ previous_collection.each { |e| ids << e["id"] if e["id"] }
50
+ current_collection.each { |e| ids << e["id"] if e["id"] }
51
+
52
+ if ids.empty?
53
+ changes[path] = [previous_collection, current_collection]
54
+ return
55
+ end
56
+
57
+ ids.each do |id|
58
+ previous_value = previous_collection.find { |r| r["id"] == id } || {}
59
+ current_value = current_collection.find { |r| r["id"] == id } || {}
60
+ previous_value.delete("id")
61
+ current_value.delete("id")
62
+
63
+ append_changes(changes, previous_value, current_value, path + [id])
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Snappier
6
+ class FilePersistence
7
+ def persist(type:, id:, at:, args:)
8
+ path = File.join("tmp", "snappier", type, id, "#{at}.yml")
9
+ FileUtils.mkdir_p(File.dirname(path))
10
+ File.write(path, args.to_yaml)
11
+ end
12
+
13
+ def each(type:, id:)
14
+ dir_path = File.join("tmp", "snappier", type.to_s, id)
15
+ Dir["#{dir_path}/*.yml"].each do |file_path|
16
+ milliseconds = File.basename(file_path).split(".").first
17
+ yield(
18
+ {
19
+ at: Time.at(milliseconds.to_i / 1000),
20
+ content: YAML.load_file(file_path)
21
+ }
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require "sidekiq"
5
+ require "yaml"
6
+
7
+ module Snappier
8
+ class Job
9
+ include Sidekiq::Job
10
+
11
+ def perform(args)
12
+ type = args["type"]
13
+ id = args["id"]
14
+ at = args["at"]
15
+
16
+ args["at"] = Time.at(at.to_i / 1000).to_s
17
+ args["state"] = JSON.parse(args["state"])
18
+
19
+ Registry.persistence.persist(
20
+ type: type,
21
+ id: id,
22
+ at: at,
23
+ args: args,
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "file_persistence"
4
+
5
+ module Snappier
6
+ module Registry
7
+ def self.reset
8
+ @for_class_name = {}
9
+ @persistence = nil
10
+ end
11
+
12
+ def self.all
13
+ @for_class_name
14
+ end
15
+
16
+ def self.register_persistence(instance)
17
+ @persistence = instance
18
+ end
19
+
20
+ def self.persistence
21
+ @persistence || FilePersistence.new
22
+ end
23
+
24
+ def self.register(map)
25
+ @for_class_name ||= {}
26
+ map.each_key do |key|
27
+ @for_class_name[key] = map[key]
28
+ end
29
+ end
30
+
31
+ def self.for_entity(entity)
32
+ @for_class_name ||= {}
33
+ class_name = @for_class_name[entity.class.name]
34
+ return unless class_name
35
+
36
+ class_name.split("::").reduce(Object) { |acc, elem| acc.const_get(elem) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "changes"
5
+
6
+ module Snappier
7
+ module Replay
8
+ def self.for(type:, id:)
9
+ previous_state = {}
10
+
11
+ Registry.persistence.each(type: type, id: id) do |current|
12
+ content = current.delete(:content)
13
+ current[:who] = content.delete("who")
14
+ current[:state] = content.delete("state")
15
+ current[:changes] = Changes.between(previous_state, current[:state])
16
+
17
+ yield(current)
18
+
19
+ previous_state = current[:state]
20
+ end
21
+ end
22
+
23
+ def self.for_entity(entity)
24
+ self.for(type: entity.class.name, id: entity.id)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "job"
5
+ require_relative "who"
6
+ require "json"
7
+
8
+ module Snappier
9
+ module Take
10
+ def self.for(entity)
11
+ action = "update"
12
+ action = "create" if entity.previously_new_record?
13
+ action = "destroy" if entity.destroyed?
14
+
15
+ snapper = Registry.for_entity(entity)
16
+ state = snapper ? snapper.snap(entity) : entity.attributes
17
+
18
+ Job.perform_async(
19
+ "at" => Time.now.strftime("%s%L"),
20
+ "action" => action,
21
+ "id" => entity.id,
22
+ "type" => entity.class.name,
23
+ "state" => state.to_json,
24
+ "who" => Who.current,
25
+ )
26
+
27
+ entity
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snappier
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snappier
4
+ module Who
5
+ def self.current=(value)
6
+ Thread.current[:snappier_who_current] = value
7
+ end
8
+
9
+ def self.current
10
+ Thread.current[:snappier_who_current]
11
+ end
12
+ end
13
+ end
data/lib/snappier.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "snappier/version"
4
+ require_relative "snappier/take"
5
+ require_relative "snappier/replay"
4
6
 
5
7
  module Snappier
6
8
  class Error < StandardError; end
metadata CHANGED
@@ -1,17 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snappier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Ryall
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 2025-05-25 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sidekiq
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  email:
13
27
  - mark@ryall.name
14
- executables: []
28
+ executables:
29
+ - snappier_replay
15
30
  extensions: []
16
31
  extra_rdoc_files: []
17
32
  files:
@@ -21,8 +36,16 @@ files:
21
36
  - LICENSE.txt
22
37
  - README.md
23
38
  - Rakefile
39
+ - exe/snappier_replay
24
40
  - lib/snappier.rb
41
+ - lib/snappier/changes.rb
42
+ - lib/snappier/file_persistence.rb
43
+ - lib/snappier/job.rb
44
+ - lib/snappier/registry.rb
45
+ - lib/snappier/replay.rb
46
+ - lib/snappier/take.rb
25
47
  - lib/snappier/version.rb
48
+ - lib/snappier/who.rb
26
49
  - sig/snappier.rbs
27
50
  homepage: https://github.com/markryall/snappier
28
51
  licenses:
@@ -30,6 +53,7 @@ licenses:
30
53
  metadata:
31
54
  homepage_uri: https://github.com/markryall/snappier
32
55
  source_code_uri: https://github.com/markryall/snappier
56
+ rubygems_mfa_required: 'true'
33
57
  rdoc_options: []
34
58
  require_paths:
35
59
  - lib