ar_doc_store 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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