trax_model 0.0.92 → 0.0.93

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/README.md +227 -86
  4. data/lib/trax.rb +0 -1
  5. data/lib/trax/model.rb +23 -29
  6. data/lib/trax/model/attributes.rb +3 -1
  7. data/lib/trax/model/attributes/attribute.rb +11 -0
  8. data/lib/trax/model/attributes/definitions.rb +16 -0
  9. data/lib/trax/model/attributes/errors.rb +8 -0
  10. data/lib/trax/model/attributes/fields.rb +74 -0
  11. data/lib/trax/model/attributes/mixin.rb +48 -19
  12. data/lib/trax/model/attributes/type.rb +4 -0
  13. data/lib/trax/model/attributes/types/array.rb +8 -25
  14. data/lib/trax/model/attributes/types/boolean.rb +51 -0
  15. data/lib/trax/model/attributes/types/enum.rb +53 -12
  16. data/lib/trax/model/attributes/types/json.rb +36 -33
  17. data/lib/trax/model/attributes/types/string.rb +50 -0
  18. data/lib/trax/model/attributes/types/uuid_array.rb +17 -28
  19. data/lib/trax/model/attributes/value.rb +16 -0
  20. data/lib/trax/model/errors.rb +7 -0
  21. data/lib/trax/model/mixins.rb +11 -0
  22. data/lib/trax/model/mixins/field_scopes.rb +60 -0
  23. data/lib/trax/model/mixins/id_scopes.rb +36 -0
  24. data/lib/trax/model/mixins/sort_by_scopes.rb +25 -0
  25. data/lib/trax/model/railtie.rb +1 -0
  26. data/lib/trax/model/scopes.rb +16 -0
  27. data/lib/trax/model/struct.rb +168 -14
  28. data/lib/trax/model/unique_id.rb +14 -21
  29. data/lib/trax/model/uuid.rb +1 -1
  30. data/lib/trax/validators/enum_attribute_validator.rb +9 -0
  31. data/lib/trax/validators/future_validator.rb +1 -1
  32. data/lib/trax/validators/json_attribute_validator.rb +3 -3
  33. data/lib/trax/validators/string_attribute_validator.rb +17 -0
  34. data/lib/trax_model/version.rb +1 -1
  35. data/spec/db/database.yml +16 -0
  36. data/spec/db/schema/default_tables.rb +68 -0
  37. data/spec/db/schema/pg_tables.rb +27 -0
  38. data/spec/spec_helper.rb +20 -3
  39. data/spec/support/models.rb +123 -0
  40. data/spec/support/pg/models.rb +103 -0
  41. data/spec/trax/model/attributes/fields_spec.rb +88 -0
  42. data/spec/trax/model/attributes/types/enum_spec.rb +51 -0
  43. data/spec/trax/model/attributes/types/json_spec.rb +107 -0
  44. data/spec/trax/model/attributes_spec.rb +13 -0
  45. data/spec/trax/model/errors_spec.rb +1 -2
  46. data/spec/trax/model/mixins/field_scopes_spec.rb +7 -0
  47. data/spec/trax/model/struct_spec.rb +1 -1
  48. data/spec/trax/model/unique_id_spec.rb +1 -3
  49. data/spec/trax/validators/url_validator_spec.rb +1 -1
  50. data/trax_model.gemspec +4 -4
  51. metadata +57 -19
  52. data/lib/trax/model/config.rb +0 -16
  53. data/lib/trax/model/validators.rb +0 -15
  54. data/lib/trax/validators/enum_validator.rb +0 -16
  55. data/spec/support/schema.rb +0 -151
  56. data/spec/trax/model/config_spec.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d24f88faa95bde183c2a7046d8c1d86b604987bd
4
- data.tar.gz: 4c124fcd2c0f21166cfc7012b75979af8551d4b7
3
+ metadata.gz: f90a589c224b12e3d814f1d0000f97ae92abf3a0
4
+ data.tar.gz: 3805c04cfb881d04c38bbeddc52ff028250cd7b1
5
5
  SHA512:
6
- metadata.gz: 5cac8b9cf60e27ab5c532ba6dc7a778319440392240cb38083676cdcaa10466446e8981fac14bca8397132ecc88f006fd551da8e040116a653e5897049cf23c2
7
- data.tar.gz: 89309b58c39783f84f322664750a05214b9522a7d7be892bcc0a00b269077563404c4622a5c549e57d4fdedd64fe5e7be949266e774279676118b234db2085d2
6
+ metadata.gz: df15b25f69f3dd4ab7a5bf1ae9b67ff16a9868a64812e6ccfad1190758a0af053aa77ce5e0edf9c226fb5246315b675242202714ca08138a38a27863e2808972
7
+ data.tar.gz: 483dace95c58f209f3474258263b4b18d5f6f4cf8ad11c00ca4290a061f0217856aeab45c992ca819447136bd8aa558e302229bc3aa127b755efb8c14a79e575
data/Gemfile CHANGED
@@ -1,3 +1,2 @@
1
1
  source 'https://rubygems.org'
2
- gem 'trax_core', :path => "~/gems/trax_core"
3
2
  gemspec
data/README.md CHANGED
@@ -1,8 +1,200 @@
1
1
  # Trax Model
2
+ A higher level, even more opinionated active record model. Some of the features are postgres specific, but library itself should work with anything. Just include ::Trax::Model module and you're off to the races. The library currently contains two major components, a declarative, explicit attribute definitions dsl, and mixins. It also has additional STI support, but don't use the MTI stuff that's getting ripped out.
2
3
 
3
- A composeable companion to active record models. Just include ::Trax::Model and you're off to the races.
4
+ ## Attributes
4
5
 
5
- ### UUIDS
6
+ An declarative, more explicit attributes dsl for your models. Biggest feature at the moment is
7
+ support for struct (json), fields, as well as enum (integer) fields.
8
+
9
+ ``` ruby
10
+ class Post
11
+ define_attributes do
12
+ enum :category, :default => :tutorials do
13
+ define :tutorials, 1
14
+ define :rants, 2
15
+ define :news, 3
16
+ end
17
+
18
+ struct :custom_fields do
19
+ boolean :is_published
20
+
21
+ enum :subtype do
22
+ define :video, 1
23
+ define :text, 2
24
+ define :audio, 3
25
+ end
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ### Struct Field (json/jsonb) ###
32
+
33
+ Finally, JSON fields that are usable. Usable as in, if you wanted to use a json field
34
+ for anything before, you probably soon after trying to use it, ran into at least one of the following problems:
35
+
36
+ 1. Cant validate it's structure. You almost always want to define the structure of the thing you are allowing into your database. Otherwise its useless
37
+ 2. Cant validate the components within it's structure. (even more difficult)
38
+ 3. Setting from user input/how the database casts it is messy to implement and prone to error
39
+
40
+ So you realize, hey what a waste of time, Ill just create a new model because thats by far a better solution than doing all the above. However,
41
+ there are alot of cases where this will end up making your application messier via unnecessary relations.
42
+
43
+ **The solution**
44
+ ``` ruby
45
+ struct :custom_fields do
46
+ string :title
47
+ boolean :is_published
48
+
49
+ enum :subtype, :define_scopes => true do
50
+ define :video, 1
51
+ define :text, 2
52
+ define :audio, 3
53
+ end
54
+
55
+ validates :title, :presence => true
56
+ end
57
+ ```
58
+
59
+ Getting/setting values works via hash, or via method calls, both work the same.
60
+
61
+ ``` ruby
62
+ #access should be indifferent so you can handle user input
63
+ ::Post.new(:custom_fields => { :subtype => :video, :is_published => false })
64
+
65
+ #or
66
+ post = ::Post.first
67
+ post.custom_fields.subtype = :audio
68
+ post.save
69
+ ```
70
+
71
+ Since struct is an actual value object, it has its own validation state. So you could call:
72
+ ``` ruby
73
+ post.custom_fields.valid?
74
+ post.custom_field.errors
75
+ ```
76
+
77
+ However, validation errors get pushed up to the root object as well, to make it easy to deal with.
78
+
79
+ ``` ruby
80
+ Post.by_custom_fields_subtype(:video, :audio)
81
+ ```
82
+
83
+ Yes thats right, you can search by the nested enum field via a search scope. It's a pretty dumb search scope (only supports enums ATM, no greater than or less than or anything that requires casting at the moment, and I really encourage structured i.e. enums to use when using struct to search).
84
+
85
+ **Warning** Use sparingly if you are doing heavy/many searches in json fields. I have no idea what performance impact to expect from lack of actual benchmarking atm and not a ton of information on pg json field search benchmarks in general, but common sense would say that if you are doing alot of searching on a ton of different values within the json data, particularly if the structures are huge, its probably going to be an expensive query.
86
+
87
+ Basically what Im saying is, if you allow a single json field to have say a 30mb json object in your db, filled with any number of possible keys and values, whenever you search that table (indexing aside), you're going to have a problem since postgres needs to look through all the col/rows in that table, + that giant field to look for matches to your query. We can reason without much knowledge of PG internals, that this is probably going to be slow.
88
+
89
+ Remember, just because you can do something, doesn't mean you should.
90
+
91
+ **With that said, giving your json fields structure, will give you better control over what you allow in the field, thereby making the search more usable. You can ensure that only the keys specified are allowed on that json field (much like a database table), and in the case of enums/boolean even limit the possible values of those keys, while providing meaning since it acts like a normal enum field.**
92
+
93
+ **Requirements to use struct field**
94
+ Fairly postgres specific, and intended to be used with the json field type. It may work with other implementations, but
95
+ this library is built to be opinionated and not handle every circumstance. -- Also use a jsonb field (pg 9.4 +)
96
+ if you want the search scope magic.
97
+
98
+ ##Enum Field (integer) ##
99
+
100
+ You may be thiking, whats wrong with rails's built in enum? Answer: Everything. Ill detail somewhere else later, for now,
101
+ just know that the enum field type wont pollute your model with a million methods like rails enum.
102
+ It also supports setting the enum value by the name of the key, or by its integer value.
103
+
104
+ Syntax:
105
+
106
+ ``` ruby
107
+ define_attributes do
108
+ enum :category, :default => :tutorials do
109
+ define :tutorials, 1
110
+ define :rants, 2
111
+ define :news, 3
112
+ end
113
+ end
114
+ ```
115
+
116
+ Only one scope method will be defined (unlike rails which defines a scope for every value
117
+ within your enum, as well as a thousand instance methods. And if you use the same value
118
+ in a different enum field on the same model, you're not going to have a good time.
119
+
120
+ Assuming a subtype enum as above, you will have the following method which accepts
121
+ multiple enum args as input.
122
+
123
+ ``` ruby
124
+ Post.by_subtype(:video, :text)
125
+ => Post.where(:subtype => [1, 2])
126
+ ```
127
+
128
+ ## Mixins
129
+
130
+ Mixins are one of the core features of Trax model. A mixin is like a concern,
131
+ (in fact, mixins extend concern, so they have that behavior as well), but
132
+ with a more rigid pattern, with configurability built in. You can pass in options
133
+ to your mixin, which will allow you to use those options to define methods and what not
134
+ based on the options passed to the mixed_in method. Example:
135
+
136
+ ``` ruby
137
+ module Slugify
138
+ extend ::Trax::Model::Mixin
139
+
140
+ mixed_in do |*field_names|
141
+ field_names.each do |field_name|
142
+ define_slug_method_for_field(field_name)
143
+ end
144
+ end
145
+
146
+ def some_instance_method
147
+ puts "Because I extend ActiveSupport::Concern"
148
+ puts "I am included into post instance methods"
149
+ end
150
+
151
+ module ClassMethods
152
+ def find_by_slug(field, *args)
153
+ where(:field => args.map(&:paramaterize))
154
+ end
155
+
156
+ private
157
+ def define_slug_method_for_field(field_name)
158
+ define_method("#{field_name}=") do |value|
159
+ super(value.paramaterize)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ ```
165
+
166
+ You would call the mixin via:
167
+
168
+ ``` ruby
169
+ class Post
170
+ mixins :slugify => [ :title, :category ]
171
+ end
172
+ ```
173
+
174
+ or
175
+ ``` ruby
176
+ class Post
177
+ mixin :slugify, :title, :category
178
+ end
179
+ ```
180
+
181
+ ``` ruby
182
+ Post.find_by_slug(:title, "Some Title")
183
+ Post.find_by_slug(:category, "Some ")
184
+ ```
185
+
186
+ The mixins dsl should look familiar to you since it acts much like "validates". However,
187
+ unlike validators, there is one registry with one list of keys. So the first paramater of
188
+ the mixin call dictate what mixin gets invoked, and if you overwrite a mixin with same name,
189
+ it will call the last one defined.
190
+
191
+ # Packaged Trax Model Mixins
192
+
193
+ ### UniqueId
194
+
195
+ ``` ruby
196
+ mixins :unique_id => { :uuid_prefix => "0a" }
197
+ ```
6
198
 
7
199
  Supports uuid prefixes, and recommends next uuid prefix based on all uuid prefixes defined
8
200
  in system -- Makes your uuids more discoverable and allows you to identify the model
@@ -35,12 +227,15 @@ the first 2 generated characters of the uuid function with a fixed character str
35
227
  may affect the stats slightly, however Im not even sure if thats in a negative manner,
36
228
  based on the fact that it splits the likeleyhood of a collision per record type
37
229
 
38
- I.E.
230
+ Usage
39
231
 
40
232
  ``` ruby
41
233
  class Product < ActiveRecord::Base
42
234
  include ::Trax::Model
43
- defaults :uuid_prefix => "0a"
235
+
236
+ mixins :unique_id => {
237
+ :uuid_prefix => "0a"
238
+ }
44
239
  end
45
240
  ```
46
241
 
@@ -50,14 +245,6 @@ Product.new
50
245
  => #<Product id: nil, name: nil, category_id: nil, user_id: nil, price: nil, in_stock_quantity: nil, on_order_quantity: nil, active: nil, uuid: "0a97ad3e-1673-41f3-b356-d62dd53629d8", created_at: nil, updated_at: nil>
51
246
  ```
52
247
 
53
- ### Get next available prefix, useful when setting models up
54
-
55
- ``` ruby
56
- bx rails c
57
- ::Trax::Model::Registry.next_prefix
58
- => "1a"
59
- ```
60
-
61
248
  ### Or, register prefixes using dsl rather than in each individual class
62
249
 
63
250
  ``` ruby
@@ -68,8 +255,6 @@ Trax::Model::UUID.register do
68
255
  end
69
256
  ```
70
257
 
71
- But wait theires more!
72
-
73
258
  ### UUID utility methods
74
259
 
75
260
  ``` ruby
@@ -80,88 +265,44 @@ product_uuid.record_type
80
265
  => Product
81
266
  product_uuid.record
82
267
  ```
268
+ Will return the product instance, Which opens up quite a few possibilites via the newfound discoverability of your uuids...
83
269
 
84
- will return the product instance
85
-
86
- Which opens up quite a few possibilites via the newfound discoverability of your uuids...
87
-
88
- # MTI (Multiple Table Inheritance)
89
-
90
- ### Note: you must use Trax UUIDS w/ prefixes to use this feature (as we map each entity to its specific table, via the prefixed uuid.
91
-
92
- Going to be a very brief documentation but:
93
-
94
- ### Set up MTI structure like this:
95
- ```
96
- models/post.rb (your entity model)
97
- models/post_types/abstract.rb (abstract, inherit from this)
98
- models/post_types/entity.rb
99
- models/post_types/video.rb
100
- models/post_types/text.rb
101
- models/post_types/audio.rb
102
- ```
103
-
104
- Post is your entity class, entity is essentially a flat table which contains a list of
105
- any common attributes, as well as the ids for each of your MTI data models. The beauty
106
- of this, is that since Trax model uuids tell us what type the record is, we don't
107
- need to use STI, or have a type column to determine the type of the record.
108
-
109
- Basically, the entity model when loaded, will eager load the real model. If the real model is created, updated, or destroyed, a callback will ensure that the corresponding entity record is kept in sync.
110
-
270
+ ## Field Scopes ##
111
271
  ``` ruby
112
- module Blog
113
- class Post < ActiveRecord::Base
114
- include ::Trax::Model::MTI::Entity
115
-
116
- mti_namespace ::Blog::Posts
117
- end
118
- end
119
-
120
- #no database table to this class as its abstract.
121
- module Blog
122
- module Posts
123
- class Abstract < ::ActiveRecord::Base
124
- # following line sets abstract_class = true when including module
125
- include ::Trax::Model::MTI::Abstract
126
-
127
- entity_model :class_name => "Blog::Post"
128
-
129
- ### Define your base inherited logic / methods here ###
130
-
131
- belongs_to :user_id
132
- belongs_to :category_id
133
-
134
- validates :user_id
135
- validates :category_id
136
- end
137
- end
138
- end
139
-
140
- module Blog
141
- module Posts
142
- class Video < ::Blog::Posts::Abstract
143
-
144
- end
145
- end
146
- end
272
+ mixins :field_scopes => {
273
+ :by_id => true,
274
+ :by_id_not => { :field => :id, :type => :not },
275
+ :by_name_matches => { :field => :name, :type => :matches }
276
+ }
147
277
  ```
148
278
 
149
- ### MTI VS STI
279
+ Here's a quick protip to writing better rails code. Treat the where method as private at all times. Use scopes to define the fields that can be searched, and keep them composable and chainable. Most search scopes should simply equate to "where field contains any number of these values". It's (generalizing) roughly the same performance hit to search one field for 100 values as it is to search one field for one value provided that value is at the bottom of the table.
150
280
 
151
- The main advantages of Multiple Table Inheritance versus Single Table inheritance I see are:
281
+ Based on those rules, 3 primary scope types right now.
152
282
 
153
- 1. Table size. As databases grow, vertically adding more length to traverse the table will continue to get slower. One way to mitigate this issue would be to split up your table into multiple horizontal tables, if it makes sense for your data structure to do so (i.e. like above)
283
+ 1. where
284
+ 2. where_not
285
+ 3. matching (contains | fuzzy)
154
286
 
155
- 2. STI will get out of proportion likely. I.e. if 90% of your posts are text posts, then when you are looking for a video post, you are to some degree being slowed down by the video posts in your table. (or at least at some point when you reach past xxxxxx number of records)
287
+ I like having the by_ affix attached to search scopes in most cases, so if your field contains a by_ it will try and guess the field name based on the fact.
156
288
 
157
- 3. Further on that note, only storing what you need for each individual subset of your data, on that particular subset. I.e. if video post has a video_url attribute, but none of the other post types have that, it will keep holes out of your data tables since video_url is only on the video_posts table.
289
+ The preceeding example will do the folllowing:
158
290
 
159
- 4. Real separation at the data level of non common attributes, so you dont have to write safeguards in child classes to make sure that a value didnt slip into a field or whatever, because each child class has its own individual interpretation of the schema.
160
-
161
- The main disadvantages are:
291
+ ``` ruby
292
+ scope :by_id, lambda{|*_values|
293
+ _values.flat_compact_uniq!
294
+ where(:id => _values)
295
+ }
296
+ scope :by_id_not, lambda{|*_values|
297
+ _values.flat_compact_uniq!
298
+ where.not(:id => _values)
299
+ }
300
+ scope :by_name_matches, lambda{|*_values|
301
+ _values.flat_compact_uniq!
302
+ matching(:name => _values)
303
+ }
304
+ ```
162
305
 
163
- 1. No shared view between child types. I.E. thats what the MTI entity is for. (want to find all blog posts? You cant unless you select a type first, or are using this gem, or a postgres view or something else)
164
- 2. More difficult to setup since each child table needs its own table.
165
306
 
166
307
  # STI
167
308
 
@@ -2,5 +2,4 @@ require 'trax/core'
2
2
  require 'trax/model'
3
3
 
4
4
  module Trax
5
-
6
5
  end
@@ -2,17 +2,22 @@ require 'active_record'
2
2
  require 'default_value_for'
3
3
  require 'hashie/dash'
4
4
  require 'hashie/mash'
5
+ require 'hashie/trash'
6
+ require 'hashie/extensions/dash/indifferent_access'
5
7
  require 'simple_enum'
6
8
  require_relative './string'
7
9
  require_relative './validators/boolean_validator'
8
10
  require_relative './validators/email_validator'
9
- require_relative './validators/enum_validator'
10
11
  require_relative './validators/frozen_validator'
11
12
  require_relative './validators/future_validator'
12
- require_relative './validators/json_attribute_validator'
13
13
  require_relative './validators/subdomain_validator'
14
14
  require_relative './validators/url_validator'
15
15
 
16
+ #trax attribute specific validators
17
+ require_relative './validators/enum_attribute_validator'
18
+ require_relative './validators/json_attribute_validator'
19
+ require_relative './validators/string_attribute_validator'
20
+
16
21
  module Trax
17
22
  module Model
18
23
  extend ::ActiveSupport::Concern
@@ -29,6 +34,7 @@ module Trax
29
34
  autoload :UniqueId
30
35
  autoload :Matchable
31
36
  autoload :Mixin
37
+ autoload :Mixins
32
38
  autoload :MTI
33
39
  autoload :Restorable
34
40
  autoload :Railtie
@@ -38,12 +44,20 @@ module Trax
38
44
 
39
45
  include ::Trax::Model::Matchable
40
46
  include ::ActiveModel::Dirty
47
+ include ::Trax::Core::InheritanceHooks
41
48
 
42
49
  define_configuration_options! do
43
50
  option :auto_include, :default => false
44
51
  option :auto_include_mixins, :default => []
45
52
  end
46
53
 
54
+ #like reverse merge, only assigns attributes which have not yet been assigned
55
+ def reverse_assign_attributes(attributes_hash)
56
+ attributes_to_assign = attributes_hash.keys.reject{|_attribute_name| attribute_present?(_attribute_name) }
57
+
58
+ assign_attributes(attributes_hash.slice(attributes_to_assign))
59
+ end
60
+
47
61
  class << self
48
62
  attr_accessor :mixin_registry
49
63
  end
@@ -67,10 +81,17 @@ module Trax
67
81
  ::Trax::Model::Freezable
68
82
  ::Trax::Model::Restorable
69
83
  ::Trax::Model::UniqueId
84
+ ::Trax::Model::Mixins::FieldScopes
85
+ ::Trax::Model::Mixins::IdScopes
86
+ ::Trax::Model::Mixins::SortByScopes
70
87
  end
71
88
 
72
89
  eager_autoload_mixins!
73
90
 
91
+ def self.find_by_uuid(uuid)
92
+ ::Trax::Model::UUID.new(uuid).record
93
+ end
94
+
74
95
  included do
75
96
  class_attribute :registered_mixins
76
97
 
@@ -87,33 +108,6 @@ module Trax
87
108
  instance_variable_set(:@_after_inherited_block, block)
88
109
  end
89
110
 
90
- #the tracepoint stuff is to ensure that we call the after_inherited block not
91
- #right after the class is defined, but rather, after the class is defined and
92
- #evaluated. i.e. this allows us to do stuff like set class attributes
93
- # class_attribute :messages_class
94
- #
95
- # after_inherited do
96
- # has_many :computed_subtype_messages, :class_name => message_class
97
- # end
98
- # - Then in subklass
99
- # self.messages_class = "MySubtypeSpecifcModel"
100
-
101
- def inherited(subklass)
102
- super(subklass)
103
-
104
- if self.instance_variable_defined?(:@_after_inherited_block)
105
- trace = ::TracePoint.new(:end) do |tracepoint|
106
- if tracepoint.self == subklass
107
- trace.disable
108
-
109
- subklass.instance_eval(&self.instance_variable_get(:@_after_inherited_block))
110
- end
111
- end
112
-
113
- trace.enable
114
- end
115
- end
116
-
117
111
  def mixin(key, options = {})
118
112
  raise ::Trax::Model::Errors::MixinNotRegistered.new(
119
113
  model: self.name,