attr_json 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/.yardopts +1 -0
- data/Gemfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +8 -0
- data/bin/console +23 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +11 -0
- data/config.ru +9 -0
- data/doc_src/dirty_tracking.md +155 -0
- data/doc_src/forms.md +124 -0
- data/json_attribute.gemspec +50 -0
- data/lib/attr_json.rb +18 -0
- data/lib/attr_json/attribute_definition.rb +93 -0
- data/lib/attr_json/attribute_definition/registry.rb +93 -0
- data/lib/attr_json/model.rb +270 -0
- data/lib/attr_json/model/cocoon_compat.rb +27 -0
- data/lib/attr_json/nested_attributes.rb +92 -0
- data/lib/attr_json/nested_attributes/builder.rb +24 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +86 -0
- data/lib/attr_json/nested_attributes/writer.rb +215 -0
- data/lib/attr_json/record.rb +140 -0
- data/lib/attr_json/record/dirty.rb +281 -0
- data/lib/attr_json/record/query_builder.rb +84 -0
- data/lib/attr_json/record/query_scopes.rb +35 -0
- data/lib/attr_json/type/array.rb +55 -0
- data/lib/attr_json/type/container_attribute.rb +56 -0
- data/lib/attr_json/type/model.rb +77 -0
- data/lib/attr_json/version.rb +3 -0
- data/playground_models.rb +101 -0
- metadata +177 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'attr_json/record/query_builder'
|
2
|
+
|
3
|
+
module AttrJson
|
4
|
+
module Record
|
5
|
+
# Adds query-ing scopes into a AttrJson::Record, based
|
6
|
+
# on postgres jsonb.
|
7
|
+
#
|
8
|
+
# Has to be mixed into something that also is a AttrJson::Record please!
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class MyRecord < ActiveRecord::Base
|
12
|
+
# include AttrJson::Record
|
13
|
+
# include AttrJson::Record::QueryScopes
|
14
|
+
#
|
15
|
+
# attr_json :a_string, :string
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# some_model.jsonb_contains(a_string: "foo").first
|
19
|
+
#
|
20
|
+
# See more in {file:README} docs.
|
21
|
+
module QueryScopes
|
22
|
+
extend ActiveSupport::Concern
|
23
|
+
|
24
|
+
included do
|
25
|
+
unless self < AttrJson::Record
|
26
|
+
raise TypeError, "AttrJson::Record::QueryScopes can only be included in a AttrJson::Record"
|
27
|
+
end
|
28
|
+
|
29
|
+
scope(:jsonb_contains, lambda do |attributes|
|
30
|
+
QueryBuilder.new(self, attributes).contains_relation
|
31
|
+
end)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Type
|
3
|
+
# You can wrap any ActiveModel::Type in one of these, and it's magically
|
4
|
+
# a type representing an Array of those things, always returning
|
5
|
+
# an array of those things on cast, serialize, and deserialize.
|
6
|
+
#
|
7
|
+
# Meant for use with AttrJson::Record and AttrJson::Model, may or
|
8
|
+
# may not do something useful or without exceptions in other contexts.
|
9
|
+
#
|
10
|
+
# AttrJson::Type::Array.new(base_type)
|
11
|
+
class Array < ::ActiveModel::Type::Value
|
12
|
+
attr_reader :base_type
|
13
|
+
def initialize(base_type)
|
14
|
+
@base_type = base_type
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
@type ||= "array_of_#{base_type.type}".to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast(value)
|
22
|
+
convert_to_array(value).collect { |v| base_type.cast(v) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize(value)
|
26
|
+
convert_to_array(value).collect { |v| base_type.serialize(v) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def deserialize(value)
|
30
|
+
convert_to_array(value).collect { |v| base_type.deserialize(v) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is used only by our own keypath-chaining query stuff.
|
34
|
+
def value_for_contains_query(key_path_arr, value)
|
35
|
+
[
|
36
|
+
if key_path_arr.present?
|
37
|
+
base_type.value_for_contains_query(key_path_arr, value)
|
38
|
+
else
|
39
|
+
base_type.serialize(base_type.cast value)
|
40
|
+
end
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def convert_to_array(value)
|
46
|
+
if value.kind_of?(Hash)
|
47
|
+
[value]
|
48
|
+
else
|
49
|
+
Array(value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Type
|
3
|
+
# A type that gets applied to the AR container/store jsonb attribute,
|
4
|
+
# to do serialization/deserialization/cast using declared attr_jsons, to
|
5
|
+
# json-able values, before calling super to original json-type, which will
|
6
|
+
# actually serialize/deserialize the json.
|
7
|
+
class ContainerAttribute < (if Gem.loaded_specs["activerecord"].version.release >= Gem::Version.new('5.2')
|
8
|
+
ActiveRecord::Type::Json
|
9
|
+
else
|
10
|
+
ActiveRecord::Type::Internal::AbstractJson
|
11
|
+
end)
|
12
|
+
attr_reader :model, :container_attribute
|
13
|
+
def initialize(model, container_attribute)
|
14
|
+
@model = model
|
15
|
+
@container_attribute = container_attribute.to_s
|
16
|
+
end
|
17
|
+
def cast(v)
|
18
|
+
# this seems to be rarely/never called by AR, not sure where if ever.
|
19
|
+
h = super || {}
|
20
|
+
model.attr_json_registry.definitions.each do |attr_def|
|
21
|
+
next unless container_attribute.to_s == attr_def.container_attribute.to_s
|
22
|
+
|
23
|
+
if h.has_key?(attr_def.store_key)
|
24
|
+
h[attr_def.store_key] = attr_def.cast(h[attr_def.store_key])
|
25
|
+
elsif attr_def.has_default?
|
26
|
+
h[attr_def.store_key] = attr_def.provide_default!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
h
|
30
|
+
end
|
31
|
+
def serialize(v)
|
32
|
+
if v.nil?
|
33
|
+
return super
|
34
|
+
end
|
35
|
+
|
36
|
+
super(v.collect do |key, value|
|
37
|
+
attr_def = model.attr_json_registry.store_key_lookup(container_attribute, key)
|
38
|
+
[key, attr_def ? attr_def.serialize(value) : value]
|
39
|
+
end.to_h)
|
40
|
+
end
|
41
|
+
def deserialize(v)
|
42
|
+
h = super || {}
|
43
|
+
model.attr_json_registry.definitions.each do |attr_def|
|
44
|
+
next unless container_attribute.to_s == attr_def.container_attribute.to_s
|
45
|
+
|
46
|
+
if h.has_key?(attr_def.store_key)
|
47
|
+
h[attr_def.store_key] = attr_def.deserialize(h[attr_def.store_key])
|
48
|
+
elsif attr_def.has_default?
|
49
|
+
h[attr_def.store_key] = attr_def.provide_default!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
h
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Type
|
3
|
+
# An ActiveModel::Type representing a particular AttrJson::Model
|
4
|
+
# class, supporting casting, serialization, and deserialization from/to
|
5
|
+
# JSON-able serializable hashes.
|
6
|
+
#
|
7
|
+
# You create one with AttrJson::Model::Type.new(attr_json_model_class),
|
8
|
+
# but normally that's only done in AttrJson::Model.to_type, there isn't
|
9
|
+
# an anticipated need to create from any other place.
|
10
|
+
class Model < ::ActiveModel::Type::Value
|
11
|
+
attr_accessor :model
|
12
|
+
def initialize(model)
|
13
|
+
#TODO type check, it really better be a AttrJson::Model. maybe?
|
14
|
+
@model = model
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
model.to_param.underscore.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast(v)
|
22
|
+
if v.nil?
|
23
|
+
# important to stay nil instead of empty object, because they
|
24
|
+
# are different things.
|
25
|
+
v
|
26
|
+
elsif v.kind_of? model
|
27
|
+
v
|
28
|
+
elsif v.respond_to?(:to_hash)
|
29
|
+
# to_hash is actually the 'implicit' conversion, it really is a hash
|
30
|
+
# even though it isn't is_a?(Hash), try to_hash first before to_h,
|
31
|
+
# the explicit conversion.
|
32
|
+
model.new_from_serializable(v.to_hash)
|
33
|
+
elsif v.respond_to?(:to_h)
|
34
|
+
# TODO Maybe we ought not to do this on #to_h?
|
35
|
+
model.new_from_serializable(v.to_h)
|
36
|
+
else
|
37
|
+
# Bad input? Most existing ActiveModel::Types seem to decide
|
38
|
+
# either nil, or a base value like the empty string. They don't
|
39
|
+
# raise. So we won't either, just nil.
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def serialize(v)
|
45
|
+
if v.nil?
|
46
|
+
nil
|
47
|
+
elsif v.kind_of?(model)
|
48
|
+
v.serializable_hash
|
49
|
+
else
|
50
|
+
cast(v).serializable_hash
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def deserialize(v)
|
55
|
+
cast(v)
|
56
|
+
end
|
57
|
+
|
58
|
+
# these guys are definitely mutable, so we need this.
|
59
|
+
def changed_in_place?(raw_old_value, new_value)
|
60
|
+
serialize(new_value) != raw_old_value
|
61
|
+
end
|
62
|
+
|
63
|
+
# This is used only by our own keypath-chaining query stuff.
|
64
|
+
def value_for_contains_query(key_path_arr, value)
|
65
|
+
first_key, rest_keys = key_path_arr.first, key_path_arr[1..-1]
|
66
|
+
attr_def = model.attr_json_registry.fetch(first_key)
|
67
|
+
{
|
68
|
+
attr_def.store_key => if rest_keys.present?
|
69
|
+
attr_def.type.value_for_contains_query(rest_keys, value)
|
70
|
+
else
|
71
|
+
attr_def.serialize(attr_def.cast value)
|
72
|
+
end
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# required by our bin/console, nothing but something to play with.
|
2
|
+
|
3
|
+
require 'attr_json'
|
4
|
+
class TestModel
|
5
|
+
include AttrJson::Model
|
6
|
+
|
7
|
+
attr_json :str, :string
|
8
|
+
attr_json :int, :integer
|
9
|
+
end
|
10
|
+
|
11
|
+
class LangAndValue
|
12
|
+
include AttrJson::Model
|
13
|
+
|
14
|
+
attr_json :lang, :string, default: "en"
|
15
|
+
attr_json :value, :string
|
16
|
+
|
17
|
+
# Yes, you can use ordinary validations... I think. If not, soon.
|
18
|
+
end
|
19
|
+
|
20
|
+
class SomeLabels
|
21
|
+
include AttrJson::Model
|
22
|
+
|
23
|
+
attr_json :hello, LangAndValue.to_type, array: true
|
24
|
+
attr_json :goodbye, LangAndValue.to_type, array: true
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
class MyModel2 < ActiveRecord::Base
|
29
|
+
self.table_name = "products"
|
30
|
+
include AttrJson::Record
|
31
|
+
include AttrJson::Record::QueryScopes
|
32
|
+
|
33
|
+
# use any ActiveModel::Type types: string, integer, decimal (BigDecimal),
|
34
|
+
# float, datetime, boolean.
|
35
|
+
attr_json :my_string, :string
|
36
|
+
attr_json :my_integer, :integer
|
37
|
+
attr_json :my_datetime, :datetime
|
38
|
+
|
39
|
+
# You can have an _array_ of those things too.
|
40
|
+
attr_json :int_array, :integer, array: true
|
41
|
+
|
42
|
+
#and/or defaults
|
43
|
+
#attr_json :int_with_default, :integer, default: 100
|
44
|
+
|
45
|
+
attr_json :special_string, :string, store_key: "__my_string"
|
46
|
+
|
47
|
+
attr_json :lang_and_value, LangAndValue.to_type
|
48
|
+
# YES, you can even have an array of them
|
49
|
+
attr_json :lang_and_value_array, LangAndValue.to_type, array: true
|
50
|
+
|
51
|
+
attr_json :my_labels, SomeLabels.to_type
|
52
|
+
end
|
53
|
+
|
54
|
+
class MyEmbeddedModel
|
55
|
+
include AttrJson::Model
|
56
|
+
|
57
|
+
attr_json :str, :string
|
58
|
+
end
|
59
|
+
|
60
|
+
class MyModel < ActiveRecord::Base
|
61
|
+
self.table_name = "products"
|
62
|
+
|
63
|
+
include AttrJson::Record
|
64
|
+
include AttrJson::Record::Dirty
|
65
|
+
|
66
|
+
attr_json :str, :string
|
67
|
+
attr_json :str_array, :string, array: true
|
68
|
+
attr_json :array_of_models, MyEmbeddedModel.to_type, array: true
|
69
|
+
end
|
70
|
+
|
71
|
+
class StaticProduct < ActiveRecord::Base
|
72
|
+
self.table_name = "products"
|
73
|
+
belongs_to :product_category
|
74
|
+
end
|
75
|
+
|
76
|
+
class Product < StaticProduct
|
77
|
+
include AttrJson::Record
|
78
|
+
include AttrJson::Record::QueryScopes
|
79
|
+
include AttrJson::Record::Dirty
|
80
|
+
|
81
|
+
attr_json :title, :string
|
82
|
+
attr_json :rank, :integer
|
83
|
+
attr_json :made_at, :datetime
|
84
|
+
attr_json :time, :time
|
85
|
+
attr_json :date, :date
|
86
|
+
attr_json :dec, :decimal
|
87
|
+
attr_json :int_array, :integer, array: true
|
88
|
+
attr_json :model, TestModel.to_type
|
89
|
+
|
90
|
+
#jsonb_accessor :options, title: :string, rank: :integer, made_at: :datetime
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
class ProductCategory < ActiveRecord::Base
|
97
|
+
include AttrJson::Record
|
98
|
+
|
99
|
+
#jsonb_accessor :options, title: :string
|
100
|
+
has_many :products
|
101
|
+
end
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attr_json
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Rochkind
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5.3'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 5.0.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.3'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.14'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.14'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '10.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '10.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.5'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.5'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: database_cleaner
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.5'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.5'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: yard-activesupport-concern
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
description: |-
|
104
|
+
ActiveRecord attributes stored serialized in a json column, super smooth.
|
105
|
+
For Rails 5.0, 5.1, or 5.2. Typed and cast like Active Record. Supporting nested models,
|
106
|
+
dirty tracking, some querying (with postgres jsonb contains), and working smoothy with form builders.
|
107
|
+
|
108
|
+
Use your database as a typed object store via ActiveRecord, in the same models right next to
|
109
|
+
ordinary ActiveRecord column-backed attributes and associations. Your json-serialized attr_json
|
110
|
+
attributes use as much of the existing ActiveRecord architecture as we can.
|
111
|
+
email:
|
112
|
+
- jonathan@dnil.net
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- ".gitignore"
|
118
|
+
- ".rspec"
|
119
|
+
- ".travis.yml"
|
120
|
+
- ".yardopts"
|
121
|
+
- Gemfile
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- bin/console
|
126
|
+
- bin/rake
|
127
|
+
- bin/rspec
|
128
|
+
- bin/setup
|
129
|
+
- config.ru
|
130
|
+
- doc_src/dirty_tracking.md
|
131
|
+
- doc_src/forms.md
|
132
|
+
- json_attribute.gemspec
|
133
|
+
- lib/attr_json.rb
|
134
|
+
- lib/attr_json/attribute_definition.rb
|
135
|
+
- lib/attr_json/attribute_definition/registry.rb
|
136
|
+
- lib/attr_json/model.rb
|
137
|
+
- lib/attr_json/model/cocoon_compat.rb
|
138
|
+
- lib/attr_json/nested_attributes.rb
|
139
|
+
- lib/attr_json/nested_attributes/builder.rb
|
140
|
+
- lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb
|
141
|
+
- lib/attr_json/nested_attributes/writer.rb
|
142
|
+
- lib/attr_json/record.rb
|
143
|
+
- lib/attr_json/record/dirty.rb
|
144
|
+
- lib/attr_json/record/query_builder.rb
|
145
|
+
- lib/attr_json/record/query_scopes.rb
|
146
|
+
- lib/attr_json/type/array.rb
|
147
|
+
- lib/attr_json/type/container_attribute.rb
|
148
|
+
- lib/attr_json/type/model.rb
|
149
|
+
- lib/attr_json/version.rb
|
150
|
+
- playground_models.rb
|
151
|
+
homepage: https://github.com/jrochkind/attr_json
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata:
|
155
|
+
homepage_uri: https://github.com/jrochkind/attr_json
|
156
|
+
source_code_uri: https://github.com/jrochkind/attr_json
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubyforge_project:
|
173
|
+
rubygems_version: 2.6.13
|
174
|
+
signing_key:
|
175
|
+
specification_version: 4
|
176
|
+
summary: ActiveRecord attributes stored serialized in a json column, super smooth.
|
177
|
+
test_files: []
|