json_sti 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3624d823ce141fe74222d5ce9d2fe9dd6a07692046db37aeb18fce3a137a5b10
4
+ data.tar.gz: e6ecba2cb054e9dbab7d69ccba1f0b820125ad8cb8fff80480cd9c0bc12730c1
5
+ SHA512:
6
+ metadata.gz: 4682108fa7c68af0dfdf92e7ce35eece47c281cf57421507ff2d5509f945d80f4fa55a299e40386544d6f862afff080eca017cbe7bed9ef8781001ea52efef52
7
+ data.tar.gz: 861e476792d0fd1da19f14db46aef05c8d646870a8b61eb099cfa34a330fec2e1b288043d732261765cf984e75df3d829a26ea09ed8f2b61563316139fc07449
data/lib/json_sti.rb ADDED
@@ -0,0 +1,150 @@
1
+ require "zeitwerk"
2
+
3
+ loader = Zeitwerk::Loader.for_gem
4
+ loader.setup
5
+
6
+ require_relative "json_sti/class_master_list"
7
+ require_relative "json_sti/inheritable_seeder"
8
+
9
+ module JsonSti
10
+ extend ActiveSupport::Concern
11
+
12
+ def self.initialize_single_table_arel_helpers
13
+ JsonSti::ClassMasterList.base_class_list.each do |receiving_class_name|
14
+ receiving_class = receiving_class_name.to_s.camelize.constantize
15
+
16
+ next unless ClassMasterList.relations_lookup[receiving_class_name]
17
+
18
+ ClassMasterList.relations_lookup[receiving_class_name][:relationships].each do |relationship_to_create|
19
+ ClassMasterList.relations_lookup[relationship_to_create][:members].each do |relationship_to_create_member|
20
+ creation_class = "#{relationship_to_create.to_s.camelize}::#{relationship_to_create_member.to_s.singularize.camelize}".constantize
21
+
22
+ ar_association = receiving_class.reflect_on_all_associations.detect do |association|
23
+ association.class_name == relationship_to_create.to_s.camelize
24
+ end
25
+
26
+ receiving_class.class_eval do
27
+ if ar_association.to_s.downcase =~ /many/
28
+ # create has_many helper methods for sti subtypes
29
+ define_method "#{relationship_to_create_member.to_s.pluralize}" do
30
+ self.send(relationship_to_create.to_s.pluralize).where(type: creation_class.to_s)
31
+ end
32
+
33
+ else
34
+ # create belongs_to helper methods for sti subtypes
35
+ define_method "#{relationship_to_create_member.to_s.singularize}" do
36
+ object = self.send(relationship_to_create.to_s.singularize)
37
+ return object.class.to_s == creation_class.to_s ? object : nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ included do
47
+ def self.inherited(subclass)
48
+ begin
49
+ #check to make sure if it exists, throws error and rescues with constant creation otherwise
50
+ subclass.to_s.split("::").last.constantize
51
+ rescue
52
+ # define subclass name as constant on global object
53
+ # probably smarter way to do this? Something zeitwerk?
54
+ Object.const_set(subclass.to_s.split("::").last, subclass)
55
+
56
+ subclass.class_eval do
57
+ # patch subclasses to validate based on their schema.
58
+ # Schemas are defined in subclasses with `define_schema`
59
+ validates :module_data,
60
+ presence: false,
61
+ json: {
62
+ message: ->(errors) { errors },
63
+ schema: lambda { cleaned_schema }
64
+ }
65
+
66
+ # a helper similar to ARs `where` only for json fields
67
+ scope :jwhere, lambda { |hash| where("module_data @> ?", hash.to_json) }
68
+
69
+ def initialize(params)
70
+ super
71
+
72
+ self.class::SCHEMA["properties"].keys.each do |attr|
73
+ unless self.module_data[attr]
74
+ if self.class::SCHEMA["required"].include? attr
75
+ self.module_data[attr] = "REQUIRED: #{self.class::SCHEMA["properties"][attr]["type"]}"
76
+ else
77
+ self.module_data[attr] = nil
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def cleaned_schema
86
+ schema = self.class::SCHEMA.to_json
87
+
88
+ hash_schema = JSON.parse(schema)
89
+ properties = hash_schema["properties"]
90
+ required = schema['required']
91
+
92
+ properties.each do |property, value|
93
+ unless self.module_data[property]
94
+ unless required.include?(property)
95
+ properties.delete property
96
+ end
97
+ end
98
+ end
99
+
100
+ hash_schema["properties"] = properties
101
+ hash_schema.to_json
102
+ end
103
+ end
104
+ end
105
+
106
+ super
107
+ end
108
+
109
+ def self.descendants
110
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
111
+ end
112
+
113
+ def self.define_schema(hash)
114
+ class_eval do
115
+ const_set(:SCHEMA, hash.with_indifferent_access)
116
+
117
+ json_attrs = self::SCHEMA["properties"]
118
+
119
+ class_variable_set(:@@json_attrs, self::SCHEMA["properties"])
120
+ class_variable_set(:@@json_required, self::SCHEMA["required"])
121
+
122
+ initialize_attr_getters(json_attrs.keys)
123
+ initialize_attr_setters(json_attrs.keys)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def self.initialize_attr_getters(attr_names)
130
+ # patches including classes to have getters for their json attr names
131
+ attr_names.each do |attr_name|
132
+ define_method attr_name do
133
+ self.module_data[attr_name]
134
+ end
135
+ end
136
+ end
137
+
138
+ def self.initialize_attr_setters(attr_names)
139
+ # patches including classes to have setters for their json attr names
140
+ attr_names.each do |attr_name|
141
+ define_method "#{attr_name}=" do |new_value|
142
+ self.module_data[attr_name] = new_value
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ loader.eager_load
@@ -0,0 +1,77 @@
1
+ module JsonSti
2
+ class ClassMasterList
3
+ def self.sti_base_class_list
4
+ # a list of STI base classes which have their own tables
5
+ @@sti_base_class_list ||= build_sti_base_class_list
6
+ end
7
+
8
+ def self.relations_lookup
9
+ # a lookup table including the subclasses of each baseclasses
10
+ # and what relationships each STI class has to other STI classes
11
+ @@relations_lookup ||= build_relations_lookup
12
+ end
13
+
14
+ def self.base_class_list
15
+ # a list of STI base classes which have their own tables
16
+ @@base_class_list ||= build_base_class_list
17
+ end
18
+
19
+ private
20
+
21
+ def self.build_sti_base_class_list
22
+ ObjectSpace.
23
+ each_object(Class).
24
+ select { |klass| klass.included_modules.include? JsonSti }.
25
+ map(&:to_s).
26
+ reject { |klass| klass.include?("::") }.
27
+ map(&:underscore).
28
+ map(&:to_sym)
29
+ end
30
+
31
+ def self.build_base_class_list
32
+ models = ActiveRecord::Base.
33
+ descendants.
34
+ map(&:name).
35
+ reject { |klass| klass.include?("::") }.
36
+ reject { |klass| klass.include?("HABTM") }.
37
+ reject { |klass| klass.include?("WP") }.
38
+ map(&:underscore).
39
+ map(&:to_sym).
40
+ tap { |models| models.delete :application_record}
41
+ end
42
+
43
+ def self.build_relations_lookup
44
+ @@relations_lookup = {}
45
+
46
+ base_class_list.each do |class_name|
47
+ @@relations_lookup[class_name] = {}
48
+ build_relation_list_for_class(class_name)
49
+ build_members_list_for_class(class_name)
50
+ end
51
+
52
+ @@relations_lookup
53
+ end
54
+
55
+ def self.build_relation_list_for_class(class_name)
56
+ klass = class_name.to_s.camelize.constantize
57
+ associations = klass.reflect_on_all_associations
58
+
59
+ relation_list = associations.map(&:class_name).
60
+ map(&:to_s).
61
+ map(&:underscore).
62
+ map(&:to_sym)
63
+
64
+ @@relations_lookup[class_name][:relationships] = (relation_list & JsonSti::ClassMasterList.sti_base_class_list)
65
+ end
66
+
67
+ def self.build_members_list_for_class(class_name)
68
+ klass = class_name.to_s.camelize.constantize
69
+ members = klass.descendants.map(&:to_s).
70
+ map{ |descendant| descendant.gsub("#{class_name.to_s.camelize}::", "") }.
71
+ map(&:underscore).
72
+ map(&:to_sym)
73
+
74
+ @@relations_lookup[class_name][:members] = members
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,125 @@
1
+ module JsonSti
2
+ class InheritableSeeder
3
+ def self.seed!(num_to_create=3)
4
+ relations_lookup = JsonSti::ClassMasterList.relations_lookup
5
+
6
+ relations_lookup.each do |type, info|
7
+ info[:members].each do |member|
8
+ klass = "#{type.to_s.camelize}::#{member.to_s.camelize}".constantize
9
+ instance = klass.create
10
+
11
+ if !instance.valid?
12
+ skip_sub_object_creation = self.fix_errors_on_instance_and_determine_next_step(instance)
13
+ next if skip_sub_object_creation
14
+ end
15
+
16
+ p "Created a #{klass.to_s}"
17
+
18
+ info[:relationships].each do |relationship|
19
+ relations_lookup[relationship][:members].each do |relation_member|
20
+ self.create_relationship_for_instance(instance, relationship, relation_member, num_to_create)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.populate_attrs_for_instance!(instance, only_required=false)
28
+ klass = instance.class
29
+ json_attrs = klass.class_variable_get(:@@json_attrs)
30
+ return if json_attrs.blank?
31
+
32
+ attrs = only_required ? klass.class_variable_get(:@@json_required) : klass.class_variable_get(:@@json_attrs).keys
33
+
34
+ return if attrs.blank?
35
+
36
+ attrs.each do |attr|
37
+ type = json_attrs[attr].values
38
+ case type.first
39
+ when "string"
40
+ if type[1] && type[1] == "date"
41
+ new_val = Faker::Date.between(from: 10.days.ago, to: Date.today)
42
+ elsif type[1] && type[1] == "time"
43
+ new_val = Faker::Time.between(from: DateTime.now - 1, to: DateTime.now)
44
+ else
45
+ new_val = Faker::Marketing.buzzwords
46
+ end
47
+ when "boolean"
48
+ new_val = [true, false].sample
49
+ when "integer"
50
+ new_val = Integer(Faker::Number.within(range: 0..99))
51
+ when "number"
52
+ new_val = Faker::Number.decimal(l_digits:2, r_digits: 3)
53
+ end
54
+
55
+ instance.send((attr.to_s + "="), new_val)
56
+ end
57
+
58
+ instance.save!
59
+
60
+ instance
61
+ end
62
+
63
+ def self.generate_valid_instance_of_class!(klass, only_required=false)
64
+ populate_attrs_for_instance!(klass.new, only_required)
65
+ end
66
+
67
+ private
68
+
69
+ def self.create_relationship_for_instance(instance, relationship_type, relation_member, num_to_create)
70
+ p "Creating a #{relation_member} for a #{instance.class}"
71
+
72
+ num_to_create.times do |n|
73
+ ar_association = instance.class.reflect_on_all_associations.detect do |association|
74
+ association.class_name == relationship_type.to_s.camelize
75
+ end
76
+
77
+ if ar_association.to_s.downcase =~ /many/
78
+ relation_name = relation_member.to_s.pluralize
79
+ sub_instance = instance.send(relation_name).create
80
+ else
81
+ klass_to_create = "#{relationship_type.to_s.camelize}::#{relation_member.to_s.camelize}".constantize
82
+ sub_instance = generate_valid_instance_of_class!(klass_to_create)
83
+
84
+ sub_instance_rel_id = sub_instance.class.to_s.split("::").first.underscore + "_id="
85
+ instance.send(sub_instance_rel_id, sub_instance.id)
86
+ end
87
+
88
+ JsonSti::InheritableSeeder.populate_attrs_for_instance!(sub_instance)
89
+ end
90
+ end
91
+
92
+ def self.fix_errors_on_instance_and_determine_next_step(instance)
93
+ klass = instance.class
94
+ skip_sub_object_creation = false
95
+
96
+ instance.errors.messages.each do |error_key, error_value|
97
+ if error_key == :module_data
98
+ JsonSti::InheritableSeeder.populate_attrs_for_instance!(instance)
99
+
100
+ elsif error_value.include? "must exist"
101
+ fix_belongs_to_based_errors_for_instance(instance, error_key)
102
+ skip_sub_object_creation = true
103
+
104
+ else
105
+ p "============================="
106
+ p "There was an unhandled error on instance creation: #{error_key} : #{error_value.first}"
107
+ p "============================="
108
+ skip_sub_object_creation = true
109
+ end
110
+ end
111
+
112
+ skip_sub_object_creation
113
+ end
114
+
115
+ def self.fix_belongs_to_based_errors_for_instance(instance, error_key)
116
+ valid_sub_type = ClassMasterList.relations_lookup[error_key][:members].sample.to_s
117
+ sub_type_klass = "#{error_key.to_s.camelize}::#{valid_sub_type.camelize}".constantize
118
+
119
+ p "creating #{sub_type_klass.to_s} belongs to for #{instance.class.to_s}"
120
+ new_sub_instance = instance.send("#{error_key}=", sub_type_klass.create)
121
+
122
+ JsonSti::InheritableSeeder.populate_attrs_for_instance!(new_sub_instance)
123
+ end
124
+ end
125
+ end
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_sti
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Max
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.7
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.77'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.77'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.7'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: faker
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.7'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 4.2.0
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: '7'
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 4.2.0
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '7'
117
+ - !ruby/object:Gem::Dependency
118
+ name: activesupport
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 4.2.0
124
+ - - "<"
125
+ - !ruby/object:Gem::Version
126
+ version: '7'
127
+ type: :runtime
128
+ prerelease: false
129
+ version_requirements: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: 4.2.0
134
+ - - "<"
135
+ - !ruby/object:Gem::Version
136
+ version: '7'
137
+ - !ruby/object:Gem::Dependency
138
+ name: pg
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '1.1'
144
+ type: :runtime
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '1.1'
151
+ - !ruby/object:Gem::Dependency
152
+ name: zeitwerk
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '2.2'
158
+ type: :runtime
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '2.2'
165
+ - !ruby/object:Gem::Dependency
166
+ name: activerecord_json_validator
167
+ requirement: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: 1.3.0
172
+ type: :runtime
173
+ prerelease: false
174
+ version_requirements: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: 1.3.0
179
+ description: A common argument against STI in rails is that the tables eventually
180
+ get cluttered with class specific columns. By keeping all subtype specific attrs
181
+ in json, you completely avoid table bloat while keeping all the advantages of AR
182
+ and a relational database
183
+ email: andrew.max89@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - lib/json_sti.rb
189
+ - lib/json_sti/class_master_list.rb
190
+ - lib/json_sti/inheritable_seeder.rb
191
+ homepage:
192
+ licenses:
193
+ - MIT
194
+ metadata: {}
195
+ post_install_message:
196
+ rdoc_options: []
197
+ require_paths:
198
+ - lib
199
+ required_ruby_version: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ requirements: []
210
+ rubygems_version: 3.0.3
211
+ signing_key:
212
+ specification_version: 4
213
+ summary: A scheme for single table inheritance with ActiveRecord which puts all class
214
+ specific attributes in a json blob, and allows validations
215
+ test_files: []