attr_json 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []