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.
@@ -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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ar_doc_store.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "test/*_test.rb"
7
+ end
@@ -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
@@ -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,17 @@
1
+ module ArDocStore
2
+ module AttributeTypes
3
+
4
+ class ArrayAttribute < Base
5
+ def conversion
6
+ :to_a
7
+ end
8
+
9
+ def predicate
10
+ 'text'
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
17
+
@@ -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,15 @@
1
+ module ArDocStore
2
+ module AttributeTypes
3
+
4
+ class FloatAttribute < Base
5
+ def conversion
6
+ :to_f
7
+ end
8
+
9
+ def predicate
10
+ 'float'
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module ArDocStore
2
+ module AttributeTypes
3
+
4
+ class IntegerAttribute < Base
5
+ def conversion
6
+ :to_i
7
+ end
8
+
9
+ def predicate
10
+ 'int'
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module ArDocStore
2
+ module AttributeTypes
3
+
4
+ class StringAttribute < Base
5
+ def conversion
6
+ :to_s
7
+ end
8
+
9
+ def predicate
10
+ 'text'
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
17
+
@@ -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,7 @@
1
+ module ArDocStore
2
+ module Embedding
3
+ def self.included(base)
4
+ base.send :include, Core
5
+ end
6
+ end
7
+ end
@@ -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,11 @@
1
+ module ArDocStore
2
+
3
+ module Model
4
+
5
+ def self.included(mod)
6
+ mod.send :include, ArDocStore::Storage
7
+ mod.send :include, ArDocStore::Embedding
8
+ end
9
+
10
+ end
11
+ 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,3 @@
1
+ module ArDocStore
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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