ar_doc_store 0.0.1
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 +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +7 -0
- data/ar_doc_store.gemspec +24 -0
- data/lib/ar_doc_store.rb +29 -0
- data/lib/ar_doc_store/attribute_types/array.rb +17 -0
- data/lib/ar_doc_store/attribute_types/base.rb +20 -0
- data/lib/ar_doc_store/attribute_types/boolean.rb +24 -0
- data/lib/ar_doc_store/attribute_types/enumeration.rb +37 -0
- data/lib/ar_doc_store/attribute_types/float.rb +15 -0
- data/lib/ar_doc_store/attribute_types/integer.rb +15 -0
- data/lib/ar_doc_store/attribute_types/string.rb +17 -0
- data/lib/ar_doc_store/embeddable_model.rb +84 -0
- data/lib/ar_doc_store/embedding.rb +7 -0
- data/lib/ar_doc_store/embedding/core.rb +30 -0
- data/lib/ar_doc_store/embedding/embeds_many.rb +95 -0
- data/lib/ar_doc_store/embedding/embeds_one.rb +67 -0
- data/lib/ar_doc_store/model.rb +11 -0
- data/lib/ar_doc_store/storage.rb +150 -0
- data/lib/ar_doc_store/version.rb +3 -0
- data/test/embedded_model_attribute_test.rb +23 -0
- data/test/embedding_test.rb +33 -0
- data/test/model_attribute_access_test.rb +100 -0
- data/test/test_helper.rb +127 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2438ac2cf9e760f9b289db586f62688dec2eb781
|
4
|
+
data.tar.gz: 19817b74ce307e2e7315f93edc8d0fe40f351e6e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e760c26792d8ef0b527ad0354ed8f27ba793097131d41fa412f68c7e7f713f3394d4ae66e0a395275e0ba7cf270ba3749d72b46944a008d25fe2aa70b478216c
|
7
|
+
data.tar.gz: 23bc9fe56773dc79d6d70c2149c6fc39f1683b5db7efac20250569742de644c967397d8ece268119a56f09f7deceddad8c30128e935462f809bfafe9be038047
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 David Furber
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# ArDocStore
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ar_doc_store'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install ar_doc_store
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it ( https://github.com/[my-github-username]/ar_doc_store/fork )
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ar_doc_store/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ar_doc_store"
|
8
|
+
spec.version = ArDocStore::VERSION
|
9
|
+
spec.authors = ["David Furber"]
|
10
|
+
spec.email = ["dfurber@gorges.us"]
|
11
|
+
spec.summary = %q{A document storage gem meant for ActiveRecord PostgresQL JSON storage.}
|
12
|
+
spec.description = %q{Provides an easy way to do something that is possible in Rails but still a bit close to the metal using store_accessor: create typecasted, persistent attributes that are not columns in the database but stored in the JSON "data" column. Also supports infinite nesting of embedded models.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 4.0"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
end
|
data/lib/ar_doc_store.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "ar_doc_store/version"
|
2
|
+
require "ar_doc_store/storage"
|
3
|
+
require "ar_doc_store/embedding/embeds_one"
|
4
|
+
require "ar_doc_store/embedding/embeds_many"
|
5
|
+
require "ar_doc_store/embedding/core"
|
6
|
+
require "ar_doc_store/embedding"
|
7
|
+
require "ar_doc_store/model"
|
8
|
+
require "ar_doc_store/embeddable_model"
|
9
|
+
require "ar_doc_store/attribute_types/base"
|
10
|
+
require "ar_doc_store/attribute_types/array"
|
11
|
+
require "ar_doc_store/attribute_types/boolean"
|
12
|
+
require "ar_doc_store/attribute_types/enumeration"
|
13
|
+
require "ar_doc_store/attribute_types/float"
|
14
|
+
require "ar_doc_store/attribute_types/integer"
|
15
|
+
require "ar_doc_store/attribute_types/string"
|
16
|
+
|
17
|
+
module ArDocStore
|
18
|
+
@mappings = Hash.new
|
19
|
+
@mappings[:array] = 'ArDocStore::AttributeTypes::ArrayAttribute'
|
20
|
+
@mappings[:boolean] = 'ArDocStore::AttributeTypes::BooleanAttribute'
|
21
|
+
@mappings[:enumeration] = 'ArDocStore::AttributeTypes::EnumerationAttribute'
|
22
|
+
@mappings[:float] = 'ArDocStore::AttributeTypes::FloatAttribute'
|
23
|
+
@mappings[:integer] = 'ArDocStore::AttributeTypes::IntegerAttribute'
|
24
|
+
@mappings[:string] = 'ArDocStore::AttributeTypes::StringAttribute'
|
25
|
+
|
26
|
+
def self.mappings
|
27
|
+
@mappings
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module AttributeTypes
|
3
|
+
class Base
|
4
|
+
attr_accessor :conversion, :predicate, :options, :model, :attribute
|
5
|
+
|
6
|
+
def self.build(model, attribute, options={})
|
7
|
+
new(model, attribute, options).build
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(model, attribute, options)
|
11
|
+
@model, @attribute, @options = model, attribute, options
|
12
|
+
end
|
13
|
+
|
14
|
+
def build
|
15
|
+
model.store_attributes conversion, predicate, attribute
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module AttributeTypes
|
3
|
+
|
4
|
+
class BooleanAttribute < Base
|
5
|
+
def build
|
6
|
+
key = attribute.to_sym
|
7
|
+
model.class_eval do
|
8
|
+
store_accessor :data, key
|
9
|
+
# define_method key, -> { item = super(); item && item == true }
|
10
|
+
define_method "#{key}?".to_sym, -> { !!key }
|
11
|
+
define_method "#{key}=".to_sym, -> (value) {
|
12
|
+
res = nil
|
13
|
+
res = true if value == 'true' || value == true || value == '1' || value == 1
|
14
|
+
res = false if value == 'false' || value == false || value == '0' || value == 0
|
15
|
+
write_store_attribute(:data, key, res)
|
16
|
+
data_will_change!
|
17
|
+
}
|
18
|
+
add_ransacker(key, 'bool')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module AttributeTypes
|
3
|
+
|
4
|
+
class EnumerationAttribute < Base
|
5
|
+
|
6
|
+
def build
|
7
|
+
key = attribute.to_sym
|
8
|
+
dictionary = options[:values]
|
9
|
+
multiple = options[:multiple]
|
10
|
+
strict = options[:strict]
|
11
|
+
model.class_eval do
|
12
|
+
|
13
|
+
if multiple
|
14
|
+
attribute key, as: :array
|
15
|
+
if strict
|
16
|
+
define_method "validate_#{key}" do
|
17
|
+
value = public_send(key)
|
18
|
+
errors.add(key, :invalid) if value.is_a?(Array) && value.present? && value.reject(&:blank?).detect {|d| !dictionary.include?(d)}
|
19
|
+
end
|
20
|
+
validate "validate_#{key}".to_sym
|
21
|
+
end
|
22
|
+
# TODO should we do anything for strict option?
|
23
|
+
else
|
24
|
+
attribute key, as: :string
|
25
|
+
if strict
|
26
|
+
validates_inclusion_of key, in: dictionary, allow_blank: true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
define_singleton_method "#{key}_choices" do
|
30
|
+
dictionary
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
|
3
|
+
module EmbeddableModel
|
4
|
+
|
5
|
+
def self.included(mod)
|
6
|
+
|
7
|
+
mod.send :include, ArDocStore::Storage
|
8
|
+
mod.send :include, ArDocStore::Embedding
|
9
|
+
mod.send :include, InstanceMethods
|
10
|
+
mod.send :extend, ClassMethods
|
11
|
+
mod.send :include, ActiveModel::AttributeMethods
|
12
|
+
mod.send :include, ActiveModel::Validations
|
13
|
+
mod.send :include, ActiveModel::Conversion
|
14
|
+
mod.send :extend, ActiveModel::Naming
|
15
|
+
mod.send :include, ActiveModel::Dirty
|
16
|
+
mod.send :include, ActiveModel::Serialization
|
17
|
+
|
18
|
+
mod.class_eval do
|
19
|
+
attr_accessor :_destroy
|
20
|
+
attr_accessor :attributes
|
21
|
+
|
22
|
+
@virtual_attributes = HashWithIndifferentAccess.new
|
23
|
+
|
24
|
+
def self.virtual_attributes; @virtual_attributes; end
|
25
|
+
def self.virtual_attributes=(value); @virtual_attributes=value; end
|
26
|
+
def virtual_attributes; self.class.virtual_attributes || HashWithIndifferentAccess.new; end
|
27
|
+
|
28
|
+
delegate :as_json, to: :attributes
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
|
35
|
+
def initialize(attrs={})
|
36
|
+
attrs ||= {}
|
37
|
+
@attributes = HashWithIndifferentAccess.new
|
38
|
+
attrs.each { |key, value|
|
39
|
+
key = "#{key}=".to_sym
|
40
|
+
self.public_send(key, value) if methods.include?(key)
|
41
|
+
}
|
42
|
+
virtual_attributes.keys.each do |attr|
|
43
|
+
@attributes[attr] ||= nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def persisted?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"#{self.class}: #{attributes.inspect}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_store_attribute(store, key)
|
56
|
+
@attributes[key]
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_store_attribute(store, key, value)
|
60
|
+
changed_attributes[key] = read_store_attribute(:data, key)
|
61
|
+
@attributes[key] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def data_will_change!
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
|
72
|
+
def store_accessor(store, key)
|
73
|
+
self.virtual_attributes ||= HashWithIndifferentAccess.new
|
74
|
+
virtual_attributes[key] = true
|
75
|
+
key = key.to_sym
|
76
|
+
define_method key, -> { read_store_attribute(:data, key) }
|
77
|
+
define_method "#{key}=".to_sym, -> (value) { write_store_attribute :data, key, value }
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module Embedding
|
3
|
+
module Core
|
4
|
+
|
5
|
+
def self.included(mod)
|
6
|
+
mod.send :include, EmbedsOne
|
7
|
+
mod.send :include, EmbedsMany
|
8
|
+
mod.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
|
13
|
+
# Returns whether or not the association is valid and applies any errors to
|
14
|
+
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
15
|
+
# enabled records if they're marked_for_destruction? or destroyed.
|
16
|
+
def embed_valid?(assn_name, record)
|
17
|
+
unless valid = record.valid?
|
18
|
+
record.errors.each do |attribute, message|
|
19
|
+
attribute = "#{assn_name}.#{attribute}"
|
20
|
+
errors[attribute] << message
|
21
|
+
errors[attribute].uniq!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
valid
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module Embedding
|
3
|
+
module EmbedsMany
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
# Validate the embedded records
|
12
|
+
def validate_embeds_many(assn_name)
|
13
|
+
if records = public_send(assn_name)
|
14
|
+
records.each { |record| embed_valid?(assn_name, record) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
|
22
|
+
def embeds_many(assn_name, *args)
|
23
|
+
store_accessor :data, assn_name
|
24
|
+
options = args.extract_options!
|
25
|
+
class_name = options[:class_name] || assn_name.to_s.classify
|
26
|
+
create_embeds_many_accessors(assn_name, class_name)
|
27
|
+
create_embed_many_attributes_method(assn_name)
|
28
|
+
create_embeds_many_validation(assn_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_embeds_many_accessors(assn_name, class_name)
|
34
|
+
define_method assn_name.to_sym, -> {
|
35
|
+
ivar = "@#{assn_name}"
|
36
|
+
existing = instance_variable_get(ivar)
|
37
|
+
return existing if existing
|
38
|
+
my_class_name = class_name.constantize
|
39
|
+
items = read_store_attribute(:data, assn_name)
|
40
|
+
if items.present? && items.first.respond_to?(:keys)
|
41
|
+
items = items.map { |item| my_class_name.new(item) }
|
42
|
+
end
|
43
|
+
items ||= []
|
44
|
+
instance_variable_set ivar, (items)
|
45
|
+
items
|
46
|
+
}
|
47
|
+
define_method "#{assn_name}=".to_sym, -> (values) {
|
48
|
+
if values
|
49
|
+
items = values.map { |item|
|
50
|
+
my_class_name = class_name.constantize
|
51
|
+
item.is_a?(my_class_name) ? item : my_class_name.new(item)
|
52
|
+
}
|
53
|
+
else
|
54
|
+
items = []
|
55
|
+
end
|
56
|
+
instance_variable_set "@#{assn_name}", write_store_attribute(:data, assn_name, items)
|
57
|
+
# data_will_change!
|
58
|
+
}
|
59
|
+
define_method "build_#{assn_name.to_s.singularize}", -> (attributes=nil) {
|
60
|
+
assns = self.public_send assn_name
|
61
|
+
item = class_name.constantize.new attributes
|
62
|
+
assns << item
|
63
|
+
public_send "#{assn_name}=", assns
|
64
|
+
item
|
65
|
+
}
|
66
|
+
|
67
|
+
define_method "ensure_#{assn_name.to_s.singularize}", -> {
|
68
|
+
public_send "build_#{assn_name.to_s.singularize}" if self.public_send(assn_name).blank?
|
69
|
+
}
|
70
|
+
# TODO: alias here instead of show the same code twice?
|
71
|
+
define_method "ensure_#{assn_name}", -> {
|
72
|
+
public_send "build_#{assn_name.to_s.singularize}" if self.public_send(assn_name).blank?
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_embed_many_attributes_method(assn_name)
|
77
|
+
define_method "#{assn_name}_attributes=", -> (values) {
|
78
|
+
data_will_change!
|
79
|
+
values = values.andand.values || []
|
80
|
+
values = values.reject { |item| item['_destroy'] == '1' }
|
81
|
+
public_send "#{assn_name}=", values
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_embeds_many_validation(assn_name)
|
86
|
+
validate_method = "validate_embedded_record_for_#{assn_name}"
|
87
|
+
define_method validate_method, -> { validate_embeds_many assn_name }
|
88
|
+
validate validate_method
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module Embedding
|
3
|
+
module EmbedsOne
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
|
12
|
+
def validate_embeds_one(assn_name)
|
13
|
+
record = public_send(assn_name)
|
14
|
+
embed_valid?(assn_name, record) if record
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def embeds_one(assn_name, *args)
|
22
|
+
store_accessor :data, assn_name
|
23
|
+
options = args.extract_options!
|
24
|
+
class_name = options[:class_name] || assn_name.to_s.classify
|
25
|
+
store_attribute_from_class class_name, assn_name
|
26
|
+
create_embed_one_attributes_method(assn_name)
|
27
|
+
create_embeds_one_accessors assn_name, class_name
|
28
|
+
create_embeds_one_validation(assn_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_embeds_one_accessors(assn_name, class_name)
|
34
|
+
define_method "build_#{assn_name}", -> (attributes=nil) {
|
35
|
+
class_name = class_name.constantize if class_name.respond_to?(:constantize)
|
36
|
+
public_send "#{assn_name}=", class_name.new(attributes)
|
37
|
+
public_send assn_name
|
38
|
+
}
|
39
|
+
define_method "ensure_#{assn_name}", -> {
|
40
|
+
public_send "build_#{assn_name}" if public_send(assn_name).blank?
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_embed_one_attributes_method(assn_name)
|
45
|
+
define_method "#{assn_name}_attributes=", -> (values) {
|
46
|
+
# data_will_change!
|
47
|
+
values ||= {}
|
48
|
+
values.symbolize_keys! if values.respond_to?(:symbolize_keys!)
|
49
|
+
if values[:_destroy] && (values[:_destroy] == '1')
|
50
|
+
self.public_send "#{assn_name}=", nil
|
51
|
+
else
|
52
|
+
public_send "#{assn_name}=", values
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_embeds_one_validation(assn_name)
|
58
|
+
validate_method = "validate_embedded_record_for_#{assn_name}"
|
59
|
+
define_method validate_method, -> { validate_embeds_one assn_name }
|
60
|
+
validate validate_method
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module ArDocStore
|
2
|
+
module Storage
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.send :include, InstanceMethods
|
6
|
+
mod.send :extend, ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
|
11
|
+
def write_attribute(name, value)
|
12
|
+
if is_stored_attribute?(name)
|
13
|
+
write_store_attribute :data, attribute_name_from_foreign_key(name), value
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_attribute(name)
|
20
|
+
if is_stored_attribute?(name)
|
21
|
+
read_store_attribute :data, attribute_name_from_foreign_key(name)
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def is_stored_attribute?(name)
|
30
|
+
name = name.to_sym
|
31
|
+
is_store_accessor_method?(name) || name =~ /data\-\>\>/
|
32
|
+
end
|
33
|
+
|
34
|
+
def is_store_accessor_method?(name)
|
35
|
+
name = name.to_sym
|
36
|
+
self.class.stored_attributes[:data] && self.class.stored_attributes[:data].include?(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def attribute_name_from_foreign_key(name)
|
40
|
+
is_store_accessor_method?(name) ? name : name.match(/\'(\w+)\'/)[0].gsub("'", '')
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
|
47
|
+
def attribute(name, *args)
|
48
|
+
type = args.shift if args.first.is_a?(Symbol)
|
49
|
+
options = args.extract_options!
|
50
|
+
type ||= options.delete(:as) || :string
|
51
|
+
class_name = ArDocStore.mappings[type]
|
52
|
+
unless const_defined?(class_name)
|
53
|
+
raise "Invalid attribute type: #{name}"
|
54
|
+
end
|
55
|
+
class_name = class_name.constantize
|
56
|
+
class_name.build self, name, options
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_ransacker(key, predicate = nil)
|
60
|
+
return unless respond_to?(:ransacker)
|
61
|
+
ransacker key do
|
62
|
+
sql = "(data->>'#{key}')"
|
63
|
+
if predicate
|
64
|
+
sql = "#{sql}::#{predicate}"
|
65
|
+
end
|
66
|
+
Arel.sql(sql)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def store_attributes(typecast_method, predicate=nil, attributes=[])
|
71
|
+
attributes = [attributes] unless attributes.respond_to?(:each)
|
72
|
+
attributes.each do |key|
|
73
|
+
store_accessor :data, key
|
74
|
+
add_ransacker(key, predicate)
|
75
|
+
if typecast_method.is_a?(Symbol)
|
76
|
+
store_attribute_from_symbol typecast_method, key
|
77
|
+
else
|
78
|
+
store_attribute_from_class typecast_method, key
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def store_attribute_from_symbol(typecast_method, key)
|
84
|
+
define_method key.to_sym, -> {
|
85
|
+
value = read_store_attribute(:data, key)
|
86
|
+
value.public_send(typecast_method) if value
|
87
|
+
}
|
88
|
+
define_method "#{key}=".to_sym, -> (value) {
|
89
|
+
# data_will_change! if @initalized
|
90
|
+
write_store_attribute(:data, key, value.public_send(typecast_method))
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def store_attribute_from_class(class_name, key)
|
95
|
+
define_method key.to_sym, -> {
|
96
|
+
ivar = "@#{key}"
|
97
|
+
existing = instance_variable_get ivar
|
98
|
+
existing || begin
|
99
|
+
item = read_store_attribute(:data, key)
|
100
|
+
class_name = class_name.constantize if class_name.respond_to?(:constantize)
|
101
|
+
item = class_name.new(item) unless item.is_a?(class_name)
|
102
|
+
instance_variable_set ivar, item
|
103
|
+
item
|
104
|
+
end
|
105
|
+
}
|
106
|
+
define_method "#{key}=".to_sym, -> (value) {
|
107
|
+
ivar = "@#{key}"
|
108
|
+
class_name = class_name.constantize if class_name.respond_to?(:constantize)
|
109
|
+
value = class_name.new(value) unless value.is_a?(class_name)
|
110
|
+
instance_variable_set ivar, value
|
111
|
+
write_store_attribute :data, key, value
|
112
|
+
# data_will_change! if @initialized
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def string_attributes(*args)
|
117
|
+
args.each do |arg|
|
118
|
+
attribute arg, as: :string
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def float_attributes(*args)
|
123
|
+
args.each do |arg|
|
124
|
+
attribute arg, as: :float
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def integer_attributes(*args)
|
129
|
+
args.each do |arg|
|
130
|
+
attribute arg, as: :integer
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def boolean_attributes(*args)
|
135
|
+
args.each do |arg|
|
136
|
+
attribute arg, as: :boolean
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def enumerates(field, *args)
|
141
|
+
options = args.extract_options!
|
142
|
+
options[:as] = :enumeration
|
143
|
+
attribute field, options
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
|
3
|
+
class EmbeddedModelAttributeTest < MiniTest::Test
|
4
|
+
|
5
|
+
def test_can_set_attribute_on_embedded_model_init
|
6
|
+
b = Route.new route_surface: 'test'
|
7
|
+
assert_equal 'test', b.route_surface
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_can_set_attribute_on_existing_embedded_model
|
11
|
+
b = Route.new
|
12
|
+
b.route_surface = 'test'
|
13
|
+
assert_equal 'test', b.route_surface
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_can_set_enumeration_created_with_enumerates
|
17
|
+
door = Door.new
|
18
|
+
door.door_type = %w{sliding push}
|
19
|
+
assert_equal %w{sliding push}, door.door_type
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
|
3
|
+
class EmbeddingTest < MiniTest::Test
|
4
|
+
|
5
|
+
def test_can_build_embedded_model
|
6
|
+
restroom = Restroom.new
|
7
|
+
door = restroom.build_door
|
8
|
+
assert door.is_a?(Door)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_ensure_door_returns_existing_door
|
12
|
+
restroom = Restroom.new
|
13
|
+
restroom.build_door
|
14
|
+
restroom.door.open_handle = %w{knob}
|
15
|
+
restroom.ensure_door
|
16
|
+
assert_equal %w{knob}, restroom.door.open_handle
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_attributes_equals_sets_attributes
|
20
|
+
restroom = Restroom.new door_attributes: { clear_distance: 5, opening_force: 13, clear_space: 43 }
|
21
|
+
assert_equal 5, restroom.door.clear_distance
|
22
|
+
restroom.door_attributes = { _destroy: '1' }
|
23
|
+
assert_nil restroom.door.clear_distance
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_attribute_validity_of_embedded_model_from_model
|
27
|
+
b = Building.new
|
28
|
+
r = Restroom.new
|
29
|
+
b.restrooms << r
|
30
|
+
assert !b.valid?
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
|
3
|
+
class ModelAttributeAccessTest < MiniTest::Test
|
4
|
+
def test_string_attribute_on_model_init
|
5
|
+
b = Building.new name: 'test'
|
6
|
+
assert_equal 'test', b.name
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_string_attribute_on_existing_model
|
10
|
+
b = Building.new
|
11
|
+
b.name = 'test'
|
12
|
+
assert_equal 'test', b.name
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_boolean_attribute_on_model_init
|
16
|
+
b = Building.new finished: true
|
17
|
+
assert b.finished?
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_boolean_attribute_on_existing_model
|
21
|
+
b = Building.new
|
22
|
+
b.finished = true
|
23
|
+
assert b.finished?
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_float_attribute_on_init
|
27
|
+
b = Building.new height: 54.45
|
28
|
+
assert_equal 54.45, b.height
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_float_attribute_on_existing_model
|
32
|
+
b = Building.new
|
33
|
+
b.height = 54.45
|
34
|
+
assert_equal 54.45, b.height
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_int_attribute_on_init
|
38
|
+
b = Building.new stories: 5
|
39
|
+
assert_equal 5, b.stories
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_int_attribute_on_set
|
43
|
+
b = Building.new
|
44
|
+
b.stories = 5
|
45
|
+
assert_equal 5, b.stories
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_simple_enumeration_attribute
|
49
|
+
b = Building.new construction: 'wood'
|
50
|
+
assert_equal 'wood', b.construction
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_multiple_enumeration_attribute
|
54
|
+
b = Building.new multiconstruction: %w{wood plaster}
|
55
|
+
assert_equal %w{wood plaster}, b.multiconstruction
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_strict_enumeration_attribute_invalid
|
59
|
+
b = Building.new strict_enumeration: 'wood'
|
60
|
+
b.valid?
|
61
|
+
assert b.errors[:strict_enumeration]
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_unstrict_enumeration_attribute_allows_assignment_of_choice_not_in_the_list
|
65
|
+
b = Building.new construction: 'plastic'
|
66
|
+
assert_equal 'plastic', b.construction
|
67
|
+
b.valid?
|
68
|
+
assert b.errors[:construction].empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_unstrict_multi_enumeration_attribute_allows_assignment_of_choice_not_in_the_list
|
72
|
+
b = Building.new multiconstruction: %w{plastic wood}
|
73
|
+
assert_equal %w{plastic wood}, b.multiconstruction
|
74
|
+
b.valid?
|
75
|
+
assert b.errors[:multiconstruction].empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_strict_multi_enumeration_attribute_invalid
|
79
|
+
b = Building.new strict_multi_enumeration: %w{good wood}
|
80
|
+
b.valid?
|
81
|
+
assert b.errors[:strict_multi_enumeration]
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_strict_enumeration_attribute_valid
|
85
|
+
b = Building.new strict_enumeration: 'glad'
|
86
|
+
b.valid?
|
87
|
+
assert b.errors[:strict_enumeration].empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_strict_multi_enumeration_attribute_valid
|
91
|
+
b = Building.new strict_multi_enumeration: %w{glad bad}
|
92
|
+
b.valid?
|
93
|
+
assert b.errors[:strict_multi_enumeration].empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_enumeration_has_choices_to_use_for_select
|
97
|
+
assert Building.construction_choices.present?
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
gem 'activerecord'
|
2
|
+
gem 'minitest'
|
3
|
+
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'active_record'
|
6
|
+
|
7
|
+
require_relative './../lib/ar_doc_store'
|
8
|
+
|
9
|
+
# A building has many entrances and restrooms and some fields of its own
|
10
|
+
# An entrance has a door, a route, and some fields of its own
|
11
|
+
# A restroom has a door, a route, and some fields measuring the stalls
|
12
|
+
# Route and door
|
13
|
+
|
14
|
+
# This here is just to mock out enough AR behavior for a model to pretend to be an AR model without a database...
|
15
|
+
class ARDuck
|
16
|
+
include ActiveModel::AttributeMethods
|
17
|
+
include ActiveModel::Validations
|
18
|
+
include ActiveModel::Conversion
|
19
|
+
extend ActiveModel::Naming
|
20
|
+
include ActiveModel::Dirty
|
21
|
+
include ActiveModel::Serialization
|
22
|
+
|
23
|
+
attr_accessor :attributes
|
24
|
+
|
25
|
+
def initialize(attrs=nil)
|
26
|
+
@attributes = HashWithIndifferentAccess.new
|
27
|
+
return if attrs.nil?
|
28
|
+
attrs.each { |key, value|
|
29
|
+
key = "#{key}=".to_sym
|
30
|
+
self.public_send(key, value) if methods.include?(key)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def persisted?
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
"#{self.class}: #{attributes.inspect}"
|
40
|
+
end
|
41
|
+
|
42
|
+
delegate :as_json, to: :attributes
|
43
|
+
|
44
|
+
def self.store_accessor(store, key)
|
45
|
+
key = key.to_sym
|
46
|
+
define_method key, -> { read_store_attribute(:data, key) }
|
47
|
+
define_method "#{key}=".to_sym, -> (value) { write_store_attribute :data, key, value }
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_store_attribute(store, key)
|
51
|
+
@attributes[key]
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_store_attribute(store, key, value)
|
55
|
+
changed_attributes[key] = read_store_attribute(:data, key)
|
56
|
+
@attributes[key] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def data_will_change!
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
class Dimensions
|
66
|
+
include ArDocStore::EmbeddableModel
|
67
|
+
attribute :length, :float
|
68
|
+
attribute :width, :float
|
69
|
+
end
|
70
|
+
|
71
|
+
class Route
|
72
|
+
include ArDocStore::EmbeddableModel
|
73
|
+
attribute :is_route_unobstructed, as: :boolean
|
74
|
+
attribute :is_route_lighted, as: :boolean
|
75
|
+
attribute :route_surface, as: :string
|
76
|
+
attribute :route_slope_percent, as: :integer
|
77
|
+
attribute :route_min_width, as: :integer
|
78
|
+
end
|
79
|
+
|
80
|
+
class Door
|
81
|
+
include ArDocStore::EmbeddableModel
|
82
|
+
enumerates :door_type, multiple: true, values: %w{single double french sliding push pull}
|
83
|
+
attribute :open_handle, as: :enumeration, multiple: true, values: %w{push pull plate knob handle}
|
84
|
+
attribute :close_handle, as: :enumeration, multiple: true, values: %w{push pull plate knob handle}
|
85
|
+
attribute :clear_distance, as: :integer
|
86
|
+
attribute :opening_force, as: :integer
|
87
|
+
attribute :clear_space, as: :integer
|
88
|
+
end
|
89
|
+
|
90
|
+
class Entrance
|
91
|
+
include ArDocStore::EmbeddableModel
|
92
|
+
embeds_one :route
|
93
|
+
embeds_one :door
|
94
|
+
end
|
95
|
+
|
96
|
+
class Restroom
|
97
|
+
include ArDocStore::EmbeddableModel
|
98
|
+
embeds_one :route
|
99
|
+
embeds_one :door
|
100
|
+
|
101
|
+
enumerates :restroom_type, values: %w{single double dirty nasty clean}
|
102
|
+
|
103
|
+
attribute :is_restroom_provided, as: :boolean
|
104
|
+
attribute :is_signage_clear, as: :boolean
|
105
|
+
|
106
|
+
embeds_one :stall_area_dimensions, class_name: 'Dimensions'
|
107
|
+
embeds_one :sink_area_dimensions, class_name: 'Dimensions'
|
108
|
+
|
109
|
+
validates :restroom_type, presence: true
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
class Building < ARDuck
|
114
|
+
include ArDocStore::Model
|
115
|
+
attribute :name, :string
|
116
|
+
attribute :comments, as: :string
|
117
|
+
attribute :finished, :boolean
|
118
|
+
attribute :stories, as: :integer
|
119
|
+
attribute :height, as: :float
|
120
|
+
attribute :construction, as: :enumeration, values: %w{concrete wood brick plaster steel}
|
121
|
+
attribute :multiconstruction, as: :enumeration, values: %w{concrete wood brick plaster steel}, multiple: true
|
122
|
+
attribute :strict_enumeration, as: :enumeration, values: %w{happy sad glad bad}, strict: true
|
123
|
+
attribute :strict_multi_enumeration, as: :enumeration, values: %w{happy sad glad bad}, multiple: true, strict: true
|
124
|
+
embeds_many :entrances
|
125
|
+
embeds_many :restrooms
|
126
|
+
end
|
127
|
+
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ar_doc_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Furber
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-08 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: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: 'Provides an easy way to do something that is possible in Rails but still
|
56
|
+
a bit close to the metal using store_accessor: create typecasted, persistent attributes
|
57
|
+
that are not columns in the database but stored in the JSON "data" column. Also
|
58
|
+
supports infinite nesting of embedded models.'
|
59
|
+
email:
|
60
|
+
- dfurber@gorges.us
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- ".gitignore"
|
66
|
+
- Gemfile
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- ar_doc_store.gemspec
|
71
|
+
- lib/ar_doc_store.rb
|
72
|
+
- lib/ar_doc_store/attribute_types/array.rb
|
73
|
+
- lib/ar_doc_store/attribute_types/base.rb
|
74
|
+
- lib/ar_doc_store/attribute_types/boolean.rb
|
75
|
+
- lib/ar_doc_store/attribute_types/enumeration.rb
|
76
|
+
- lib/ar_doc_store/attribute_types/float.rb
|
77
|
+
- lib/ar_doc_store/attribute_types/integer.rb
|
78
|
+
- lib/ar_doc_store/attribute_types/string.rb
|
79
|
+
- lib/ar_doc_store/embeddable_model.rb
|
80
|
+
- lib/ar_doc_store/embedding.rb
|
81
|
+
- lib/ar_doc_store/embedding/core.rb
|
82
|
+
- lib/ar_doc_store/embedding/embeds_many.rb
|
83
|
+
- lib/ar_doc_store/embedding/embeds_one.rb
|
84
|
+
- lib/ar_doc_store/model.rb
|
85
|
+
- lib/ar_doc_store/storage.rb
|
86
|
+
- lib/ar_doc_store/version.rb
|
87
|
+
- test/embedded_model_attribute_test.rb
|
88
|
+
- test/embedding_test.rb
|
89
|
+
- test/model_attribute_access_test.rb
|
90
|
+
- test/test_helper.rb
|
91
|
+
homepage: ''
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.4.5
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: A document storage gem meant for ActiveRecord PostgresQL JSON storage.
|
115
|
+
test_files:
|
116
|
+
- test/embedded_model_attribute_test.rb
|
117
|
+
- test/embedding_test.rb
|
118
|
+
- test/model_attribute_access_test.rb
|
119
|
+
- test/test_helper.rb
|