quantum_fields 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +74 -0
- data/Rakefile +17 -0
- data/lib/quantum_fields.rb +38 -0
- data/lib/quantum_fields/active_record_patch.rb +47 -0
- data/lib/quantum_fields/no_sqlize.rb +87 -0
- data/lib/quantum_fields/railtie.rb +4 -0
- data/lib/quantum_fields/validation_injector.rb +24 -0
- data/lib/quantum_fields/version.rb +3 -0
- data/lib/tasks/quantum_fields_tasks.rake +4 -0
- metadata +99 -0
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,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
|
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: []
|