snapshotable 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +145 -0
- data/lib/generators/snapshotable/create_generator.rb +59 -0
- data/lib/generators/snapshotable/templates/migration.rb +17 -0
- data/lib/generators/snapshotable/templates/model.rb +6 -0
- data/lib/services/snapshot_creator.rb +53 -0
- data/lib/snapshotable/version.rb +3 -0
- data/lib/snapshotable.rb +62 -0
- data/lib/tasks/cacheable_models.rake +0 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c6e8b118f2e16f01a786078f2f595dc4758d68a9
|
4
|
+
data.tar.gz: ce248ffcc575d4958f117be3558551a311a82c3a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4734984d20167bc564e2314d4333947fb6b33e3639584d94c0c976b4714dbebf33ae4154f4da044941fd73414054a5ad931b277090611bd2333d4530a7f51d8e
|
7
|
+
data.tar.gz: a4fbbbb53540e83ccd1a19451286f00194ca69dbc669cb8e57994d3d5dae582538d8366c155ce4b127d79dcc1a0a21e089131578798f0b090eaffee5da5cb3cd
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Manuel Puyol
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# Snapshotable
|
4
|
+
|
5
|
+
This gem is intended to work as a model history, saving important information about it (and its relations) over time.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'snapshotable'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install snapshotable
|
22
|
+
|
23
|
+
## Setup
|
24
|
+
|
25
|
+
### Creating the model and migration
|
26
|
+
We provide a helpful generator to create migrations and models easily. Simply run:
|
27
|
+
```
|
28
|
+
rails generate snapshotable:create <model_to_be_snapshoted>
|
29
|
+
```
|
30
|
+
|
31
|
+
Example:
|
32
|
+
```
|
33
|
+
rails generate snapshotable:create user
|
34
|
+
```
|
35
|
+
|
36
|
+
This would create a `create_user_snapshot` migration
|
37
|
+
```
|
38
|
+
class CreateContractSnapshots < ActiveRecord::Migration
|
39
|
+
def change
|
40
|
+
create_table :user_snapshots do |t|
|
41
|
+
# model to be snapshoted
|
42
|
+
t.references :user, index: true, null: false, foreign_key: true
|
43
|
+
|
44
|
+
# snapshoted_attributes
|
45
|
+
t.jsonb :attributes, null: false
|
46
|
+
t.timestamps null: false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
and a `UserSnapshot` model
|
53
|
+
```
|
54
|
+
class UserSnapshot < ApplicationRecord
|
55
|
+
belongs_to :user
|
56
|
+
|
57
|
+
validates :user, presence: true
|
58
|
+
validates :attributes, presence: true
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### Saving relation information
|
63
|
+
|
64
|
+
It's possible to add `--has_one` or `--has_many` options to the generator, which creates the needed attributes
|
65
|
+
|
66
|
+
Example:
|
67
|
+
```
|
68
|
+
rails generate snapshotable:create user --has_one profile photo --has_many groups friends
|
69
|
+
```
|
70
|
+
|
71
|
+
This would create a `create_user_snapshot` migration
|
72
|
+
```
|
73
|
+
class CreateContractSnapshots < ActiveRecord::Migration
|
74
|
+
def change
|
75
|
+
create_table :user_snapshots do |t|
|
76
|
+
# model to be snapshoted
|
77
|
+
t.references :user, index: true, null: false, foreign_key: true
|
78
|
+
|
79
|
+
# snapshoted_attributes
|
80
|
+
t.jsonb :attributes, null: false
|
81
|
+
|
82
|
+
t.jsonb :profile_attributes, null: false
|
83
|
+
t.jsonb :photo_attributes, null: false
|
84
|
+
|
85
|
+
t.jsonb :groups_attributes, null: false, array: true, default: []
|
86
|
+
t.jsonb :friends_attributes, null: false, array: true, default: []
|
87
|
+
|
88
|
+
t.timestamps null: false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
In this case, the model won't change, but you could modify it manually if you wish.
|
95
|
+
|
96
|
+
### Setting the base model
|
97
|
+
|
98
|
+
After creating the Snapshot model, add a `has_many` relation to the base model
|
99
|
+
```
|
100
|
+
has_many user_snapshots, dependent: :destroy
|
101
|
+
```
|
102
|
+
Then, set which attributes should be saved on the Snapshot using `snapshot` on the model
|
103
|
+
```
|
104
|
+
snapshot :id, :name, :age, profile: [:description], photo: [:url], groups: [:name], friends: [:name, :age]
|
105
|
+
```
|
106
|
+
|
107
|
+
### Creating a Snapshot
|
108
|
+
|
109
|
+
Run `take_snapshot!` to save a new snapshot from a model instance. This will only save a new snapshot if any of the saved fields have changed.
|
110
|
+
|
111
|
+
In the example above a `UserSnapshot` would be created like this:
|
112
|
+
```
|
113
|
+
{
|
114
|
+
user_id: user.id,
|
115
|
+
attributes: {
|
116
|
+
id: user.id,
|
117
|
+
name: user.name,
|
118
|
+
age: user.age
|
119
|
+
},
|
120
|
+
profile_attributes: {
|
121
|
+
description: user.profile.description
|
122
|
+
},
|
123
|
+
photo_attributes: {
|
124
|
+
url: user.photo.url
|
125
|
+
},
|
126
|
+
groups_attributes: [{
|
127
|
+
name: user.groups.first.name
|
128
|
+
},
|
129
|
+
{
|
130
|
+
name: user.groups.second.name
|
131
|
+
}],
|
132
|
+
friends_attributes: [{
|
133
|
+
name: user.friends.first.name,
|
134
|
+
age: user.friends.first.age
|
135
|
+
},
|
136
|
+
{
|
137
|
+
name: user.friends.second.name,
|
138
|
+
age: user.friends.second.age
|
139
|
+
}]
|
140
|
+
}
|
141
|
+
```
|
142
|
+
|
143
|
+
## License
|
144
|
+
|
145
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/migration"
|
3
|
+
require "active_record"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
module Snapshotable
|
7
|
+
module Generators
|
8
|
+
class CreateGenerator < Rails::Generators::Base
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
argument :snapshotable_model, type: :string
|
14
|
+
class_option :has_one, type: :array, default: [], desc: 'has_one relations to add to the snapshot'
|
15
|
+
class_option :has_many, type: :array, default: [], desc: 'has_many relations to add to the snapshot'
|
16
|
+
|
17
|
+
def generate_migration_and_model
|
18
|
+
migration_template "migration.rb", "db/migrate/create_#{snapshotable_model}_snapshots.rb", migration_version: migration_version
|
19
|
+
template "model.rb", "app/models/#{model_underscored}_snapshot.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Implement the required interface for Rails::Generators::Migration.
|
23
|
+
def self.next_migration_number(dirname) #:nodoc:
|
24
|
+
next_migration_number = current_migration_number(dirname) + 1
|
25
|
+
if ActiveRecord::Base.timestamped_migrations
|
26
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
27
|
+
else
|
28
|
+
"%.3d" % next_migration_number
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def migration_version
|
33
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
34
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def active_record_class
|
39
|
+
ActiveRecord::VERSION::MAJOR >= 5 ? 'ApplicationRecord' : 'ActiveRecord::Base'
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_one
|
43
|
+
options['has_one']
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_many
|
47
|
+
options['has_many']
|
48
|
+
end
|
49
|
+
|
50
|
+
def model_underscored
|
51
|
+
snapshotable_model.underscore
|
52
|
+
end
|
53
|
+
|
54
|
+
def model_camelcased
|
55
|
+
snapshotable_model.camelcase
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= model_underscored %>_snapshots do |t|
|
4
|
+
# model to be snapshoted
|
5
|
+
t.references :<%= model_underscored %>, index: true, null: false, foreign_key: true
|
6
|
+
|
7
|
+
# snapshoted_attributes
|
8
|
+
t.jsonb :attributes, null: false
|
9
|
+
<% has_one.each do |relation| %>
|
10
|
+
t.jsonb :<%= relation.underscore %>_attributes, null: false<% end %>
|
11
|
+
<% has_many.each do |relation| %>
|
12
|
+
t.jsonb :<%= relation.underscore %>_attributes, null: false, array: true, default: []<% end %>
|
13
|
+
|
14
|
+
t.timestamps null: false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Snapshotable
|
2
|
+
class SnapshotCreator
|
3
|
+
def initialize(record)
|
4
|
+
@record = record
|
5
|
+
end
|
6
|
+
|
7
|
+
def call
|
8
|
+
snapshot_attrs
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
attr_reader :record
|
14
|
+
|
15
|
+
def snapshot_attrs
|
16
|
+
snapshot = {}
|
17
|
+
|
18
|
+
record.custom_snapshot_attributes.each do |key, attribute|
|
19
|
+
snapshot[key] = record.send(attribute)
|
20
|
+
end
|
21
|
+
|
22
|
+
snapshot[:attributes] = extract_attributes(record_snapshot_attrs, record) if record_snapshot_attrs.any?
|
23
|
+
|
24
|
+
deep_snapshot_attrs&.each do |association_name, attributes|
|
25
|
+
association = record.send(association_name)
|
26
|
+
|
27
|
+
snapshot["#{association_name}_attributes"] = if association.class.name == 'ActiveRecord::Associations::CollectionProxy'
|
28
|
+
association.map { |model| extract_attributes(attributes, model) }
|
29
|
+
else
|
30
|
+
extract_attributes(attributes, association)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
snapshot
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_attributes(attributes, model)
|
38
|
+
return {} if model.blank?
|
39
|
+
|
40
|
+
attributes.inject({}) do |collected_attrs, attr|
|
41
|
+
collected_attrs.merge(attr => model.send(attr))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def record_snapshot_attrs
|
46
|
+
@record_snapshot_attrs ||= record.attributes_to_save_on_snapshot.select { |attr| attr.is_a? Symbol }
|
47
|
+
end
|
48
|
+
|
49
|
+
def deep_snapshot_attrs
|
50
|
+
@deep_snapshot_attrs ||= record.attributes_to_save_on_snapshot.select { |attr| attr.is_a? Hash }.first
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/snapshotable.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'services/snapshot_creator'
|
2
|
+
|
3
|
+
module Snapshotable
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
class_attribute :attributes_to_save_on_snapshot, instance_writer: false
|
8
|
+
class_attribute :attributes_to_ignore_on_diff, instance_writer: false
|
9
|
+
class_attribute :custom_snapshot_attributes, instance_writer: false
|
10
|
+
|
11
|
+
self.attributes_to_save_on_snapshot = []
|
12
|
+
self.attributes_to_ignore_on_diff = %w[id created_at updated_at]
|
13
|
+
self.custom_snapshot_attributes = {}
|
14
|
+
|
15
|
+
unless instance_methods.include?(:take_snapshot!)
|
16
|
+
def take_snapshot!
|
17
|
+
snapshot = SnapshotCreator.new(self).call
|
18
|
+
snapshot_model = Object.const_get("#{self.class.name}Snapshot")
|
19
|
+
|
20
|
+
snapshot_model.create!(snapshot) if should_create_new_snapshot?(snapshot)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
unless instance_methods.include?(:last_snapshot)
|
25
|
+
def last_snapshot
|
26
|
+
send("#{self.class.name.downcase}_snapshots").last
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
unless instance_methods.include?(:should_create_cache?)
|
31
|
+
def should_create_new_snapshot?(snapshot)
|
32
|
+
return true if last_snapshot.nil?
|
33
|
+
|
34
|
+
snapshot_to_compare = last_snapshot
|
35
|
+
.attributes
|
36
|
+
.except(*attributes_to_ignore_on_diff)
|
37
|
+
.with_indifferent_access
|
38
|
+
|
39
|
+
HashDiff.diff(snapshot_to_compare, snapshot.with_indifferent_access).any?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
def snapshot(*attributes)
|
47
|
+
self.attributes_to_save_on_snapshot = attributes
|
48
|
+
end
|
49
|
+
|
50
|
+
def snapshot_ignore_diff(*attributes)
|
51
|
+
self.attributes_to_ignore_on_diff = attributes.map(&:to_s)
|
52
|
+
end
|
53
|
+
|
54
|
+
def custom_snapshot(*attributes)
|
55
|
+
self.custom_snapshot_attributes = attributes.first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
ActiveSupport.on_load :active_record do
|
61
|
+
include Snapshotable
|
62
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snapshotable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- João Batista Marinho
|
8
|
+
- Manuel Puyol
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-08-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.1'
|
21
|
+
- - "<"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '6'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '4.1'
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6'
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activerecord
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.1'
|
41
|
+
- - "<"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '6'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '4.1'
|
51
|
+
- - "<"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '6'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: hashdiff
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.3'
|
61
|
+
- - "<"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '1'
|
64
|
+
type: :runtime
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0.3'
|
71
|
+
- - "<"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '1'
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: bundler
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '1.16'
|
81
|
+
type: :development
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '1.16'
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: pry-rails
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0.3'
|
95
|
+
- - "<"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1'
|
98
|
+
type: :development
|
99
|
+
prerelease: false
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0.3'
|
105
|
+
- - "<"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '1'
|
108
|
+
- !ruby/object:Gem::Dependency
|
109
|
+
name: rake
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '10.0'
|
115
|
+
type: :development
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - "~>"
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '10.0'
|
122
|
+
- !ruby/object:Gem::Dependency
|
123
|
+
name: rspec
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - "~>"
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '3.0'
|
129
|
+
type: :development
|
130
|
+
prerelease: false
|
131
|
+
version_requirements: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - "~>"
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '3.0'
|
136
|
+
description: Caches a model in a time period
|
137
|
+
email: engineering@qulture.rocks
|
138
|
+
executables: []
|
139
|
+
extensions: []
|
140
|
+
extra_rdoc_files: []
|
141
|
+
files:
|
142
|
+
- LICENSE.txt
|
143
|
+
- README.md
|
144
|
+
- lib/generators/snapshotable/create_generator.rb
|
145
|
+
- lib/generators/snapshotable/templates/migration.rb
|
146
|
+
- lib/generators/snapshotable/templates/model.rb
|
147
|
+
- lib/services/snapshot_creator.rb
|
148
|
+
- lib/snapshotable.rb
|
149
|
+
- lib/snapshotable/version.rb
|
150
|
+
- lib/tasks/cacheable_models.rake
|
151
|
+
homepage: https://github.com/QultureRocks/snapshotable
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata: {}
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: 2.1.7
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 2.6.14
|
172
|
+
signing_key:
|
173
|
+
specification_version: 4
|
174
|
+
summary: Caches a model in a time period.
|
175
|
+
test_files: []
|