attr_json 0.1.0

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