quantum_fields 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: 75256efead78bb5b4cf0fb0addf73eb4ebc4e541c5a549b2f26a0708a2dbc7b6
4
+ data.tar.gz: 783c270ceb4e8d3fae1a2bb3669fe075694c5137a8816bab8d48dd10340e0621
5
+ SHA512:
6
+ metadata.gz: 319478c26780c49f9862863882239d5a11056bc8ec1c30895a1be826e4c01af04f562d3e5d72e9b9bc854cf19c13cfff815a4bf1e109635ed1e13baa0a303995
7
+ data.tar.gz: 936e0ef4b000b0370f9a060c769583e62029a820f129d47c9b9e2e49ebdaa4b8901be50db9e427ccc5ebac64ba589a1a3f6b8de5b9931e07bb2d1feb125a7597
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Fernando Bellincanta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # QuantumFields [![Build Status](https://travis-ci.com/ErvalhouS/quantum_fields.svg?branch=master)](https://travis-ci.com/ErvalhouS/quantum_fields) [![Maintainability](https://api.codeclimate.com/v1/badges/48fd7d9c967edc9327fe/maintainability)](https://codeclimate.com/github/ErvalhouS/quantum_fields/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/48fd7d9c967edc9327fe/test_coverage)](https://codeclimate.com/github/ErvalhouS/quantum_fields/test_coverage) [![Inline docs](http://inch-ci.org/github/ErvalhouS/quantum_fields.svg?branch=master)](http://inch-ci.org/github/ErvalhouS/quantum_fields)
2
+
3
+ ### Give noSQL powers to your SQL database
4
+ QuantumFields is a gem to extend your Rails model's making them able to use virtual fields from a JSON column or a Text column serialized as a Hash. This means that you can use fields that were not explicitly declared in your schema as if they were.
5
+
6
+ This feature is dependent in one of two things:
7
+
8
+ 1. Your model's table should include a Hstore, JSON or JSONB column
9
+ 2. Your model's table should include a Text column that gets serialized as a Hash
10
+
11
+ You should prioritize option 1 since it translates all queries into SQL, which is great for performance. If your database does not support Hstore, JSON or JSONB you are stuck with option 2, which depends on PORO manipulation of your datasets.
12
+
13
+ ## Usage
14
+ To add noSQL behavior into your model you just need to call `no_sqlize` on it.
15
+ ```ruby
16
+ class MyModel < ActiveRecord::Base
17
+ no_sqlize
18
+ end
19
+ ```
20
+
21
+ This initializes the behavior on default a column called `quantum_fields`. If you wish to use another column you need declare it explicitily
22
+ ```ruby
23
+ class MyModel < ActiveRecord::Base
24
+ no_sqlize fields_column: :my_json_field
25
+ end
26
+ ```
27
+
28
+ All ActiveRecord queries will now translate SQLs to QuantumFields using JSON operators, allowing you to do:
29
+ ```ruby
30
+ >> MyModel.create(a_column_that_does_not_exists: 'I do exist')
31
+ # => #<Person id: 1, quantum_fields: { a_column_that_does_not_exists: 'I do exist' }>
32
+ >> MyModel.where(a_column_that_does_not_exists: 'I do exist')
33
+ # => [ #<Person id: 1, quantum_fields: { a_column_that_does_not_exists: 'I do exist' }> ]
34
+ >> MyModel.all.order(:a_column_that_does_not_exists)
35
+ # => [ #<Person id: 1, quantum_fields: { a_column_that_does_not_exists: 'I do exist' }> ]
36
+ >> MyModel.last.a_column_that_does_not_exists
37
+ # => 'I do exist'
38
+ >> MyModel.last.a_column_that_does_not_exists = "I'm here!"
39
+ # => 'I'm here!'
40
+ # And so on...
41
+ ```
42
+
43
+ You can also perform per-record validations, which can get injected by a `quantum_rules` attribute. This means that if you set a record with a presence validator for a God QuantumField saving without it would raise an exception.
44
+ ```ruby
45
+ >> object = MyModel.new
46
+ >> object.quantum_rules = { god: { validates: { presence: true } } }
47
+ >> object.save!
48
+ # => ActiveRecord::RecordInvalid: Validation failed: God can't be blank
49
+ ```
50
+
51
+ This column is optional, but you can use any column you'd like for it by explicitily declaring in your `no_sqlize` call.
52
+ ```ruby
53
+ class MyModel < ActiveRecord::Base
54
+ no_sqlize rules_column: :my_json_field
55
+ end
56
+ ```
57
+
58
+ ## Installation
59
+ Add this line to your application's Gemfile:
60
+
61
+ ```ruby
62
+ gem 'quantum_fields'
63
+ ```
64
+
65
+ And then execute:
66
+ ```bash
67
+ $ bundle
68
+ ```
69
+
70
+ ## Contributing
71
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ErvalhouS/quantum_fields. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant code of conduct](http://contributor-covenant.org/). To find good places to start contributing, try looking into our issue list and our Codeclimate profile.
72
+
73
+ ## License
74
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'QuantumFields'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'quantum_fields/active_record_patch'
5
+ require 'quantum_fields/no_sqlize'
6
+ require 'quantum_fields/railtie'
7
+ require 'quantum_fields/validation_injector'
8
+
9
+ # A module to abstract a noSQL aproach into SQL records, using a `parameters`
10
+ # JSON column.
11
+ module QuantumFields
12
+ extend ActiveSupport::Concern
13
+ # This module contains the methods called within the Model to initialize
14
+ # and to manage how the other methods should behave on the given context.
15
+ module ClassMethods
16
+ def no_sqlize(args = {})
17
+ class_eval <<-RUBY, __FILE__, __LINE__+1
18
+ def self.fields_column
19
+ :#{args[:fields_column] || :quantum_fields}
20
+ end
21
+ def self.rules_column
22
+ :#{args[:rules_column] || :quantum_rules}
23
+ end
24
+ def as_json(*args)
25
+ super.except(self.class.fields_column.to_s, self.class.rules_column.to_s).tap do |hash|
26
+ self.try(:quantum_fields)&.each do |key, value|
27
+ hash[key] = value
28
+ end
29
+ end
30
+ end
31
+ RUBY
32
+ include QuantumFields::NoSqlize
33
+ include QuantumFields::ValidationInjector
34
+ end
35
+ end
36
+ end
37
+
38
+ ActiveRecord::Base.send(:include, QuantumFields)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::PredicateBuilder.class_eval do
4
+ def build(attribute, value)
5
+ if table.type(attribute.name).force_equality?(value)
6
+ bind = build_bind_attribute(attribute.name, value)
7
+ attribute.eq(bind)
8
+ elsif table.send('klass').respond_to?(:fields_column) && !table.send('klass').column_names.include?(attribute.name)
9
+ json_key = attribute.name
10
+ attribute.name = table.send('klass').fields_column.to_s
11
+ op = Arel::Nodes::InfixOperation.new('->>', table.send('klass').arel_table[table.send('klass').fields_column], Arel::Nodes.build_quoted(json_key))
12
+ bind = build_bind_attribute(op, value)
13
+ op.eq(bind)
14
+ else
15
+ handler_for(value).call(attribute, value)
16
+ end
17
+ end
18
+ end
19
+ ActiveRecord::Relation.class_eval do
20
+ def arel_attribute(name) # :nodoc:
21
+ if klass.column_names.include?(name.to_s) || !klass.respond_to?(:fields_column)
22
+ klass.arel_attribute(name, table)
23
+ else
24
+ Arel::Nodes::InfixOperation.new('->>',
25
+ klass.arel_table[klass.fields_column],
26
+ Arel::Nodes.build_quoted(name))
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveModel::AttributeMethods::ClassMethods.module_eval do
32
+ # Is +new_name+ an alias or a quantum field?
33
+ def attribute_alias?(new_name)
34
+ attribute_aliases.key?(new_name.to_s) ||
35
+ (!['all', 'star', ' ', '*'].include?(new_name.to_s) &&
36
+ respond_to?(:fields_column) &&
37
+ !column_names.include?(new_name.to_s))
38
+ end
39
+ # Returns the original name or quantum field for the alias +name+
40
+ def attribute_alias(name)
41
+ if column_names.include?(name.to_s) || !respond_to?(:fields_column)
42
+ attribute_aliases[name.to_s]
43
+ else
44
+ name
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuantumFields
4
+ # Module to enable virtual methods in moduls, it creates necessary
5
+ # methods to simulate the behavior that any field can exist, getting
6
+ # or setting it is like a migrated column behaves.
7
+ module NoSqlize
8
+ extend ActiveSupport::Concern
9
+ # Overriding `method_missing` does the magic of assigning an absent field
10
+ # into `.parameters` JSON as a key-value pair, or recovering it's value if
11
+ # exists.
12
+ def method_missing(meth, *args, &block)
13
+ if can_no_sqlize?
14
+ no_sqlize!(meth, *args, &block)
15
+ else
16
+ raise NotImplementedError,
17
+ "#{model.table_name} should have a `#{fields_column}` JSON column"
18
+ end
19
+ rescue StandardError
20
+ super
21
+ end
22
+
23
+ # Used to map which methods can be called in the instance, it is based on
24
+ # the premisse that you can always assign a value to any field and recover
25
+ # only existing fields or the ones that are in the `.parameters` JSON.
26
+ def respond_to_missing?(method_name, include_private = false)
27
+ meth = method_name.to_s
28
+ can_no_sqlize? && meth.ends_with?('=') || meth.ends_with?('changed?') ||
29
+ try(fields_column).try(:[], meth).present? ||
30
+ super
31
+ end
32
+
33
+ # Overriding a ActiveRecord method that is used in `.create` and `.update`
34
+ # methods to assign a value into an attribute.
35
+ def _assign_attribute(key, value)
36
+ initialize_fields
37
+ public_send("#{key}=", value) ||
38
+ try(fields_column).try(:except!, key.to_s)
39
+ end
40
+
41
+ private
42
+
43
+ # Retrieves current model class
44
+ def model
45
+ self.class
46
+ end
47
+
48
+ # Retrieves fields_column configured in current scope
49
+ def fields_column
50
+ model.fields_column
51
+ end
52
+
53
+ # Checks for `.parameters` presence and initializes it.
54
+ def initialize_fields
55
+ try("#{fields_column}=", {}) unless try(fields_column).present?
56
+ end
57
+
58
+ # The behavior that a noSQL method should have. It either assigns a value into
59
+ # an attribute or retrieves it's value, depending in the method called.
60
+ def no_sqlize!(meth, *args, &_block)
61
+ method_name = meth.to_s
62
+ initialize_fields
63
+ if method_name.ends_with?('=')
64
+ assing_to(method_name.chomp('='), args.first)
65
+ else
66
+ try(fields_column).try(:[], method_name)
67
+ end
68
+ end
69
+
70
+ # Assings a value to a key in :fields_column which allows saving any virtual
71
+ # attribute
72
+ def assing_to(param, value)
73
+ try(fields_column).try(:[]=, param, value)
74
+ end
75
+
76
+ # Checks if requirements are met for the feature
77
+ def can_no_sqlize?
78
+ model.column_names.include?(fields_column.to_s) &&
79
+ %i[json jsonb].include?(no_sqlize_column.type)
80
+ end
81
+
82
+ # Retrieve which column is being used to sqlize
83
+ def no_sqlize_column
84
+ model.columns.find { |col| col.name == fields_column.to_s }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,4 @@
1
+ module QuantumFields
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuantumFields
4
+ # This module injects a behavior on no_sqlized models that enables backend
5
+ # validations on virtual fields based on rules passed by the instance
6
+ module ValidationInjector
7
+ extend ActiveSupport::Concern
8
+ included do
9
+ before_validation :map_injected_validations
10
+ def map_injected_validations
11
+ send(self.class.rules_column).try(:deep_symbolize_keys)&.each do |field, rules|
12
+ validations = rules[:validates]
13
+ inject_validations(field, validations) if validations.present?
14
+ end
15
+ end
16
+
17
+ def inject_validations(field, validations)
18
+ validations.each do |method, value|
19
+ singleton_class.validates field, method => value
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module QuantumFields
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :quantum_fields do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quantum_fields
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Fernando Bellincanta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-02-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: QuantumFields is a gem to extend your Rails model's making them able
56
+ to use virtual fields from a JSON column or a Text column serialized as a Hash.
57
+ This means that you can use fields that were not explicitly declared in your schema
58
+ as if they were.
59
+ email:
60
+ - ervalhous@hotmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - MIT-LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - lib/quantum_fields.rb
69
+ - lib/quantum_fields/active_record_patch.rb
70
+ - lib/quantum_fields/no_sqlize.rb
71
+ - lib/quantum_fields/railtie.rb
72
+ - lib/quantum_fields/validation_injector.rb
73
+ - lib/quantum_fields/version.rb
74
+ - lib/tasks/quantum_fields_tasks.rake
75
+ homepage: https://github.com/ErvalhouS/quantum_fields
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.7.7
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Give noSQL powers to your SQL database
99
+ test_files: []