Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle serialized types #52

Merged
merged 5 commits into from
Dec 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ AllCops:
- 'spec/dummy/**/*'
- 'tmp/**/*'
- 'bench/**/*'
- 'Rakefile'
- 'Gemfile'
- '*.gemspec'
DisplayCopNames: true
StyleGuideCopsOnly: false
TargetRubyVersion: 2.3

Style/AccessorMethodName:
Naming/AccessorMethodName:
Enabled: false

Style/TrivialAccessors:
Expand All @@ -26,20 +29,30 @@ Style/Documentation:
Style/StringLiterals:
Enabled: false

Style/SpaceInsideStringInterpolation:
Layout/SpaceInsideStringInterpolation:
EnforcedStyle: no_space

Style/BlockDelimiters:
Exclude:
- 'spec/**/*.rb'

Style/PercentLiteralDelimiters:
Enabled: false

Lint/AmbiguousRegexpLiteral:
Enabled: false

Lint/MissingCopEnableDirective:
Enabled: false

Metrics/MethodLength:
Exclude:
- 'spec/**/*.rb'

Metrics/BlockLength:
Exclude:
- 'spec/**/*.rb'

Metrics/LineLength:
Max: 100
Exclude:
Expand All @@ -59,5 +72,11 @@ Lint/HandleExceptions:
Exclude:
- 'spec/**/*.rb'

Style/DotPosition:
Layout/DotPosition:
EnforcedStyle: leading

Layout/IndentHeredoc:
Enabled: false

Layout/EmptyLineAfterMagicComment:
Enabled: false
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## master

## 0.6.0 (2017-12-27)

- [Fixes [#33](https://github.com/palkan/logidze/issues/33)] Support attributes types. ([@palkan][])

Added deserialization of complex types (such as `jsonb`, arrays, whatever).

- Use positional arguments in `at`/`diff_from` methods and allow passing version. ([@palkan][])

Now you can write `post.diff_from(time: ts)`, `post.diff_from(version: x)`, Post.at(time: 1.day.ago)`, etc.

NOTE: the previous behaviour is still supported (but gonna be deprecated),
i.e. you still can use `post.diff_from(ts)` if you don't mind the deprecation warning.

## 0.5.3 (2017-08-22)

- Add `--update` flag to model migration. ([@palkan][])
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# frozen_string_literal: true
source 'https://rubygems.org'

# Specify your gem's dependencies in logidze.gemspec
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,35 +134,35 @@ post.log_version #=> 3
post.log_size #=> 3

# Get copy of a record at a given time
old_post = post.at(2.days.ago)
old_post = post.at(time: 2.days.ago)

# or revert the record itself to the previous state (without committing to DB)
post.at!('201-04-15 12:00:00')
post.at!(time: '201-04-15 12:00:00')

# If no version found
post.at('1945-05-09 09:00:00') #=> nil
post.at(time: '1945-05-09 09:00:00') #=> nil
```

You can also get revision by version number:

```ruby
post.at_version(2)
post.at(version: 2)
```

It is also possible to get version for relations:

```ruby
Post.where(active: true).at(1.month.ago)
Post.where(active: true).at(time: 1.month.ago)
```

You can also get diff from specified time:

```ruby
post.diff_from(1.hour.ago)
post.diff_from(time: 1.hour.ago)
#=> { "id" => 27, "changes" => { "title" => { "old" => "Logidze sucks!", "new" => "Logidze rulz!" } } }

# the same for relations
Post.where(created_at: Time.zone.today.all_day).diff_from(1.hour.ago)
Post.where(created_at: Time.zone.today.all_day).diff_from(time: 1.hour.ago)
```

There are also `#undo!` and `#redo!` options (and more general `#switch_to!`):
Expand Down
3 changes: 3 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rubocop/rake_task"

RuboCop::RakeTask.new
RSpec::Core::RakeTask.new(:spec)

namespace :dummy do
Expand Down
2 changes: 1 addition & 1 deletion lib/generators/logidze/model/model_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ModelGenerator < ::ActiveRecord::Generators::Base # :nodoc:

def generate_migration
if options[:blacklist] && options[:whitelist]
$stderr.puts "Use only one: --whitelist or --blacklist"
warn "Use only one: --whitelist or --blacklist"
exit(1)
end
migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
Expand Down
2 changes: 1 addition & 1 deletion lib/logidze/has_logidze.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module HasLogidze
module ClassMethods # :nodoc:
# Include methods to work with history.
#
# rubocop:disable Style/PredicateName
# rubocop:disable Naming/PredicateName, Style/MixinUsage
def has_logidze
include Logidze::Model
end
Expand Down
4 changes: 2 additions & 2 deletions lib/logidze/history.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ def as_json(options = {})
private

def build_changes(a, b)
b.each_with_object({}) do |kv, acc|
acc[kv.first] = { "old" => a[kv.first], "new" => kv.last } unless kv.last == a[kv.first]
b.each_with_object({}) do |(k, v), acc|
acc[k] = { "old" => a[k], "new" => v } unless v == a[k]
end
end

Expand Down
107 changes: 84 additions & 23 deletions lib/logidze/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
require 'active_support'

module Logidze
module Deprecations # :nodoc:
def self.show_ts_deprecation_for(meth)
warn(
"[Deprecation] Usage of #{meth}(time) will be removed in the future releases, "\
"use #{meth}(time: ts) instead"
)
end
end

# Extends model with methods to browse history
# rubocop: disable Metrics/ModuleLength
module Model
require 'logidze/history/type' if Rails::VERSION::MAJOR >= 5

Expand All @@ -20,23 +30,29 @@ module Model

module ClassMethods # :nodoc:
# Return records reverted to specified time
def at(ts)
all.map { |record| record.at(ts) }.compact
def at(ts = nil, time: nil, version: nil)
Deprecations.show_ts_deprecation_for(".at") if ts
time ||= ts
all.map { |record| record.at(time: time, version: version) }.compact
end

# Return changes made to records since specified time
def diff_from(ts)
all.map { |record| record.diff_from(ts) }
def diff_from(ts = nil, time: nil, version: nil)
Deprecations.show_ts_deprecation_for(".diff_from") if ts
time ||= ts
all.map { |record| record.diff_from(time: time, version: version) }
end

# Alias for Logidze.without_logging
def without_logging(&block)
Logidze.without_logging(&block)
end

# rubocop: disable Naming/PredicateName
def has_logidze?
true
end
# rubocop: enable Naming/PredicateName
end

# Use this to convert Ruby time to milliseconds
Expand All @@ -45,35 +61,48 @@ def has_logidze?
attr_accessor :logidze_requested_ts

# Return a dirty copy of record at specified time
# If time is less then the first version, then return nil.
# If time is greater then the last version, then return self.
def at(ts)
ts = parse_time(ts)
# If time/version is less then the first version, then return nil.
# If time/version is greater then the last version, then return self.
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
def at(ts = nil, time: nil, version: nil)
Deprecations.show_ts_deprecation_for("#at") if ts

return nil unless log_data.exists_ts?(ts)
return at_version(version) if version

if log_data.current_ts?(ts)
self.logidze_requested_ts = ts
time ||= ts
time = parse_time(time)

return nil unless log_data.exists_ts?(time)

if log_data.current_ts?(time)
self.logidze_requested_ts = time
return self
end

version = log_data.find_by_time(ts).version
version = log_data.find_by_time(time).version

object_at = dup
object_at.apply_diff(version, log_data.changes_to(version: version))
object_at.id = id
object_at.logidze_requested_ts = ts
object_at.logidze_requested_ts = time

object_at
end
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength

# Revert record to the version at specified time (without saving to DB)
def at!(ts)
ts = parse_time(ts)
return self if log_data.current_ts?(ts)
return false unless log_data.exists_ts?(ts)
def at!(ts = nil, time: nil, version: nil)
Deprecations.show_ts_deprecation_for("#at!") if ts

return at_version!(version) if version

version = log_data.find_by_time(ts).version
time ||= ts
time = parse_time(time)

return self if log_data.current_ts?(time)
return false unless log_data.exists_ts?(time)

version = log_data.find_by_time(time).version

apply_diff(version, log_data.changes_to(version: version))
end
Expand All @@ -99,11 +128,17 @@ def at_version!(version)
#
# @example
#
# post.diff_from(2.days.ago)
# post.diff_from(time: 2.days.ago) # or post.diff_from(version: 2)
# #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
def diff_from(ts)
ts = parse_time(ts)
{ "id" => id, "changes" => log_data.diff_from(time: ts) }
def diff_from(ts = nil, version: nil, time: nil)
Deprecations.show_ts_deprecation_for("#diff_from") if ts
time ||= ts
time = parse_time(time) if time
changes = log_data.diff_from(time: time, version: version).tap do |v|
deserialize_changes!(v)
end

{ "id" => id, "changes" => changes }
end

# Restore record to the previous version.
Expand Down Expand Up @@ -135,6 +170,7 @@ def switch_to!(version, append: Logidze.append_on_undo)
end
end

# rubocop: disable Metrics/MethodLength
def association(name)
association = super

Expand All @@ -157,15 +193,40 @@ def association(name)

association
end
# rubocop: enable Metrics/MethodLength

protected

def apply_diff(version, diff)
diff.each { |k, v| send("#{k}=", v) }
diff.each do |k, v|
apply_column_diff(k, v)
end

log_data.version = version
self
end

def apply_column_diff(column, value)
write_attribute column, deserialize_value(column, value)
end

if Rails::VERSION::MAJOR < 5
def deserialize_value(column, value)
@attributes[column].type.type_cast_from_database(value)
end
else
def deserialize_value(column, value)
@attributes[column].type.deserialize(value)
end
end

def deserialize_changes!(diff)
diff.each do |k, v|
v["old"] = deserialize_value(k, v["old"])
v["new"] = deserialize_value(k, v["new"])
end
end

def logidze_past?
return false unless @logidze_requested_ts

Expand Down
11 changes: 5 additions & 6 deletions lib/logidze/versioned_association.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
module Logidze
module Logidze # :nodoc: all
module VersionedAssociation
# rubocop: disable Metrics/MethodLength, Metrics/AbcSize
def load_target
target = super

Expand All @@ -10,10 +11,10 @@ def load_target

if target.is_a? Array
target.map! do |object|
object.at(time)
object.at(time: time)
end.compact!
else
target.at!(time)
target.at!(time: time)
end

target
Expand All @@ -26,9 +27,7 @@ def stale_target?
def logidze_stale?
return false if !loaded? || inversed

unless target.is_a?(Array)
return owner.logidze_requested_ts != target.logidze_requested_ts
end
return owner.logidze_requested_ts != target.logidze_requested_ts unless target.is_a?(Array)

return false if target.empty?

Expand Down
Loading