pg_tags_on 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +16 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/lib/pg_tags_on/active_record/arel.rb +66 -0
- data/lib/pg_tags_on/active_record/base.rb +41 -0
- data/lib/pg_tags_on/benchmark/benchmark.rb +52 -0
- data/lib/pg_tags_on/predicate_handler/array_integer_handler.rb +9 -0
- data/lib/pg_tags_on/predicate_handler/array_jsonb_handler.rb +31 -0
- data/lib/pg_tags_on/predicate_handler/array_jsonb_with_attrs_handler.rb +41 -0
- data/lib/pg_tags_on/predicate_handler/array_string_handler.rb +9 -0
- data/lib/pg_tags_on/predicate_handler/array_text_handler.rb +9 -0
- data/lib/pg_tags_on/predicate_handler/base_handler.rb +89 -0
- data/lib/pg_tags_on/predicate_handler.rb +64 -0
- data/lib/pg_tags_on/repositories/array_jsonb_repository.rb +88 -0
- data/lib/pg_tags_on/repositories/array_repository.rb +103 -0
- data/lib/pg_tags_on/repositories/base_repository.rb +44 -0
- data/lib/pg_tags_on/repository.rb +59 -0
- data/lib/pg_tags_on/tag.rb +31 -0
- data/lib/pg_tags_on/tags_query.rb +27 -0
- data/lib/pg_tags_on/validations/validator.rb +43 -0
- data/lib/pg_tags_on/version.rb +5 -0
- data/lib/pg_tags_on.rb +56 -0
- data/pg_tags_on.gemspec +38 -0
- data/spec/array_integers/records_spec.rb +47 -0
- data/spec/array_integers/tag_ops_spec.rb +65 -0
- data/spec/array_integers/taggings_spec.rb +27 -0
- data/spec/array_integers/tags_spec.rb +53 -0
- data/spec/array_jsonb/records_spec.rb +89 -0
- data/spec/array_jsonb/tag_ops_spec.rb +115 -0
- data/spec/array_jsonb/taggings_spec.rb +27 -0
- data/spec/array_jsonb/tags_spec.rb +41 -0
- data/spec/array_strings/records_spec.rb +61 -0
- data/spec/array_strings/tag_ops_spec.rb +65 -0
- data/spec/array_strings/taggings_spec.rb +27 -0
- data/spec/array_strings/tags_spec.rb +54 -0
- data/spec/config/database.yml +6 -0
- data/spec/configuration_spec.rb +48 -0
- data/spec/helpers/database_helpers.rb +46 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/factory.rb +47 -0
- data/spec/tags_query_spec.rb +31 -0
- data/spec/validator_spec.rb +40 -0
- data/tasks/benchmark.rake +58 -0
- metadata +260 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
class PredicateHandler
|
5
|
+
# Predicate handler for jsonb[] column type
|
6
|
+
class ArrayJsonbWithAttrsHandler < ArrayJsonbHandler
|
7
|
+
OPERATORS = {
|
8
|
+
eq: :eq,
|
9
|
+
all: '?&',
|
10
|
+
any: '?|',
|
11
|
+
in: '<@',
|
12
|
+
one: '?&'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def left
|
16
|
+
node = arel_function('array_to_json', attribute)
|
17
|
+
node = arel_cast(node, 'jsonb')
|
18
|
+
node = arel_function('jsonb_path_query_array', node, arel_build_quoted("$[*].#{key.join('.')}"))
|
19
|
+
|
20
|
+
node
|
21
|
+
end
|
22
|
+
|
23
|
+
def right
|
24
|
+
if predicate == :in
|
25
|
+
arel_cast(arel_sql("'#{value.to_json}'"), 'jsonb')
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def value
|
32
|
+
@value ||= Array.wrap(query.value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def cast_type
|
36
|
+
subtype = ActiveModel::Type::String.new
|
37
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(subtype)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
class PredicateHandler
|
5
|
+
# Base predicate handler
|
6
|
+
class BaseHandler
|
7
|
+
include PgTagsOn::ActiveRecord::Arel
|
8
|
+
|
9
|
+
OPERATORS = {
|
10
|
+
eq: :eq,
|
11
|
+
all: '@>',
|
12
|
+
any: '&&',
|
13
|
+
in: '<@',
|
14
|
+
one: '@>'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def initialize(attribute, query, predicate_builder)
|
18
|
+
@attribute = attribute
|
19
|
+
@query = query
|
20
|
+
@predicate_builder = predicate_builder
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
raise 'Invalid predicate' unless OPERATORS.keys.include?(predicate)
|
25
|
+
|
26
|
+
if operator.is_a?(Symbol)
|
27
|
+
send("#{operator}_node")
|
28
|
+
else
|
29
|
+
::Arel::Nodes::InfixOperation.new(operator, left, right)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def predicate
|
34
|
+
@predicate ||= query.predicate.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def operator
|
38
|
+
@operator ||= self.class.const_get('OPERATORS').fetch(predicate)
|
39
|
+
end
|
40
|
+
|
41
|
+
def eq_node
|
42
|
+
node = ::Arel::Nodes::InfixOperation.new(self.class::OPERATORS[:all], left, right)
|
43
|
+
node.and(arel_function('array_length', attribute, 1).eq(value.size))
|
44
|
+
end
|
45
|
+
|
46
|
+
def left
|
47
|
+
attribute
|
48
|
+
end
|
49
|
+
|
50
|
+
def right
|
51
|
+
bind_node
|
52
|
+
end
|
53
|
+
|
54
|
+
def bind_node
|
55
|
+
query_attr = ::ActiveRecord::Relation::QueryAttribute.new(attribute_name, value, cast_type)
|
56
|
+
Arel::Nodes::BindParam.new(query_attr)
|
57
|
+
end
|
58
|
+
|
59
|
+
def value
|
60
|
+
@value ||= Array.wrap(query.value)
|
61
|
+
end
|
62
|
+
|
63
|
+
def klass
|
64
|
+
@klass ||= predicate_builder.send(:table).send(:klass)
|
65
|
+
end
|
66
|
+
|
67
|
+
def table_name
|
68
|
+
@table_name ||= attribute.relation.name
|
69
|
+
end
|
70
|
+
|
71
|
+
def attribute_name
|
72
|
+
attribute.name.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns Type object
|
76
|
+
def cast_type
|
77
|
+
@cast_type ||= klass.type_for_attribute(attribute_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def settings
|
81
|
+
@settings ||= (klass.pg_tags_on_options_for(attribute_name) || {}).symbolize_keys
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :attribute, :query, :predicate_builder
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
# Models' predicate handlers register this class
|
5
|
+
class PredicateHandler < ::ActiveRecord::PredicateBuilder::BaseHandler
|
6
|
+
def call(attribute, query)
|
7
|
+
handler = Builder.new(attribute, query, predicate_builder).call
|
8
|
+
|
9
|
+
handler.call
|
10
|
+
end
|
11
|
+
|
12
|
+
# Handler builder class
|
13
|
+
class Builder
|
14
|
+
def initialize(attribute, query, predicate_builder)
|
15
|
+
@attribute = attribute
|
16
|
+
@query = query
|
17
|
+
@predicate_builder = predicate_builder
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
if column.array?
|
22
|
+
array_handler
|
23
|
+
else
|
24
|
+
BaseHandler.new(attribute, query, predicate_builder)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :attribute, :query, :predicate_builder
|
31
|
+
|
32
|
+
def klass
|
33
|
+
@klass ||= predicate_builder.send(:table).send(:klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
def column
|
37
|
+
@column ||= klass.columns_hash[attribute.name]
|
38
|
+
end
|
39
|
+
|
40
|
+
def column_type
|
41
|
+
@column_type ||= column.type.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def settings
|
45
|
+
@settings ||= (klass.pg_tags_on_options_for(attribute.name) || {}).symbolize_keys
|
46
|
+
end
|
47
|
+
|
48
|
+
def array_handler
|
49
|
+
handler_klass =
|
50
|
+
if column_type == 'jsonb'
|
51
|
+
if settings.key?(:has_attributes)
|
52
|
+
ArrayJsonbWithAttrsHandler
|
53
|
+
else
|
54
|
+
ArrayJsonbHandler
|
55
|
+
end
|
56
|
+
else
|
57
|
+
PredicateHandler.const_get("Array#{column_type.classify}Handler")
|
58
|
+
end
|
59
|
+
|
60
|
+
handler_klass.new(attribute, query, predicate_builder)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
module Repositories
|
5
|
+
# Operatons for 'jsonb[]' column type
|
6
|
+
class ArrayJsonbRepository < ArrayRepository
|
7
|
+
def create(tag)
|
8
|
+
with_normalized_tags(tag) do |n_tag|
|
9
|
+
super(n_tag)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(tag, new_tag)
|
14
|
+
with_normalized_tags(tag, new_tag) do |n_tag, n_new_tag|
|
15
|
+
sql_set = <<-SQL.strip
|
16
|
+
#{column_name}[index] = #{column_name}[index] || $2
|
17
|
+
SQL
|
18
|
+
|
19
|
+
update_tag(n_tag, sql_set, [query_attribute(n_new_tag.to_json)])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(tag)
|
24
|
+
with_normalized_tags(tag) do |n_tag|
|
25
|
+
sql_set = <<-SQL.strip
|
26
|
+
#{column_name} = #{column_name}[1:index-1] || #{column_name}[index+1:2147483647]
|
27
|
+
SQL
|
28
|
+
|
29
|
+
update_tag(n_tag, sql_set)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def with_normalized_tags(*tags, &block)
|
36
|
+
normalized_tags = Array.wrap(tags).flatten.map do |tag|
|
37
|
+
key? && Array.wrap(key).reverse.inject(tag) { |a, n| { n => a } } || tag
|
38
|
+
end
|
39
|
+
|
40
|
+
block.call(*normalized_tags)
|
41
|
+
end
|
42
|
+
|
43
|
+
def array_to_recordset
|
44
|
+
return unnest unless key?
|
45
|
+
|
46
|
+
arel_jsonb_extract_path(unnest, *key_sql)
|
47
|
+
end
|
48
|
+
|
49
|
+
def key
|
50
|
+
@key ||= options[:key]
|
51
|
+
end
|
52
|
+
|
53
|
+
def key_sql
|
54
|
+
@key_sql ||= Array.wrap(key).map { |k| Arel.sql("'#{k}'") }
|
55
|
+
end
|
56
|
+
|
57
|
+
def key?
|
58
|
+
key.present?
|
59
|
+
end
|
60
|
+
|
61
|
+
def taggings_with_ordinality_query(tag)
|
62
|
+
column = Arel::Table.new('t')['name']
|
63
|
+
value = bind_for(tag.to_json, nil)
|
64
|
+
|
65
|
+
arel_table
|
66
|
+
.project('id, name, index')
|
67
|
+
.from("#{table_name}, #{unnest_with_ordinality}")
|
68
|
+
.where(arel_infix_operation('@>', column, value))
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_tag(tag, set_sql, bindings = [])
|
72
|
+
subquery = taggings_with_ordinality_query(tag)
|
73
|
+
.where(arel_table[:id].in(arel_sql(klass.reselect('id').to_sql)))
|
74
|
+
|
75
|
+
sql = <<-SQL.strip
|
76
|
+
WITH records as ( #{subquery.to_sql} )
|
77
|
+
UPDATE #{table_name}
|
78
|
+
SET #{set_sql}
|
79
|
+
FROM records
|
80
|
+
WHERE #{table_name}.id = records.id
|
81
|
+
SQL
|
82
|
+
|
83
|
+
bindings = [query_attribute(tag.to_json)] + Array.wrap(bindings)
|
84
|
+
klass.connection.exec_query(sql, 'SQL', bindings)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
module Repositories
|
5
|
+
# This repository works with "character varying[]" and "integer[]" column types
|
6
|
+
class ArrayRepository < BaseRepository
|
7
|
+
def all
|
8
|
+
subquery = klass
|
9
|
+
.select(arel_distinct(array_to_recordset).as('name'))
|
10
|
+
.arel
|
11
|
+
.as('tags')
|
12
|
+
|
13
|
+
PgTagsOn::Tag
|
14
|
+
.select(Arel.star)
|
15
|
+
.from(subquery)
|
16
|
+
.order('tags.name') # override rails' default order by id
|
17
|
+
end
|
18
|
+
|
19
|
+
def all_with_counts
|
20
|
+
taggings
|
21
|
+
.except(:select)
|
22
|
+
.select('name, count(name) as count')
|
23
|
+
.group('name')
|
24
|
+
end
|
25
|
+
|
26
|
+
def find(tag)
|
27
|
+
all.where(name: tag).first
|
28
|
+
end
|
29
|
+
|
30
|
+
def exists?(tag)
|
31
|
+
all.exists?(tag)
|
32
|
+
end
|
33
|
+
|
34
|
+
def taggings
|
35
|
+
PgTagsOn::Tag
|
36
|
+
.select(Arel.star)
|
37
|
+
.from(taggings_query)
|
38
|
+
.order('taggings.name')
|
39
|
+
end
|
40
|
+
|
41
|
+
def count
|
42
|
+
all.count
|
43
|
+
end
|
44
|
+
|
45
|
+
def create(tag)
|
46
|
+
return true if tag.blank?
|
47
|
+
|
48
|
+
klass.update_all(column_name => arel_array_cat(arel_column, bind_for(Array.wrap(tag))))
|
49
|
+
end
|
50
|
+
|
51
|
+
def update(tag, new_tag)
|
52
|
+
return true if tag.blank? || new_tag.blank? || tag == new_tag
|
53
|
+
|
54
|
+
klass
|
55
|
+
.where(column_name => Tags.one(tag))
|
56
|
+
.update_all(column_name => arel_array_replace(arel_column, bind_for(tag), bind_for(new_tag)))
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete(tag)
|
60
|
+
klass
|
61
|
+
.where(column_name => Tags.one(tag))
|
62
|
+
.update_all(column_name => arel_array_remove(arel_column, bind_for(tag)))
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def array_to_recordset
|
68
|
+
unnest
|
69
|
+
end
|
70
|
+
|
71
|
+
def taggings_query
|
72
|
+
klass
|
73
|
+
.select(
|
74
|
+
array_to_recordset.as('name'),
|
75
|
+
arel_table['id'].as('entity_id')
|
76
|
+
)
|
77
|
+
.arel
|
78
|
+
.as('taggings')
|
79
|
+
end
|
80
|
+
|
81
|
+
def ref
|
82
|
+
"#{table_name}.#{column_name}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def unnest
|
86
|
+
arel_unnest(arel_column)
|
87
|
+
end
|
88
|
+
|
89
|
+
def unnest_with_ordinality(alias_table: 't')
|
90
|
+
"#{unnest.to_sql} WITH ORDINALITY #{alias_table}(name, index)"
|
91
|
+
end
|
92
|
+
|
93
|
+
def query_attribute(value)
|
94
|
+
arel_query_attribute(arel_column, value, cast_type)
|
95
|
+
end
|
96
|
+
|
97
|
+
def bind_for(value, attr = arel_column)
|
98
|
+
query_attr = arel_query_attribute(attr, value, cast_type)
|
99
|
+
arel_bind(query_attr)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
module Repositories
|
5
|
+
# Base class for repositories.
|
6
|
+
class BaseRepository
|
7
|
+
include ::PgTagsOn::ActiveRecord::Arel
|
8
|
+
|
9
|
+
def self.api_methods
|
10
|
+
%i[all all_with_counts find exists? taggings count create update delete to_s]
|
11
|
+
end
|
12
|
+
|
13
|
+
api_methods.each do |m|
|
14
|
+
define_method(m) do
|
15
|
+
raise 'Not implemented'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :klass, :column_name, :options
|
20
|
+
|
21
|
+
def initialize(klass, column_name, options = {})
|
22
|
+
@klass = klass
|
23
|
+
@column_name = column_name
|
24
|
+
@options = options.deep_symbolize_keys
|
25
|
+
end
|
26
|
+
|
27
|
+
def table_name
|
28
|
+
@table_name ||= klass.table_name
|
29
|
+
end
|
30
|
+
|
31
|
+
def cast_type
|
32
|
+
@cast_type ||= klass.type_for_attribute(column_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def arel_table
|
36
|
+
klass.arel_table
|
37
|
+
end
|
38
|
+
|
39
|
+
def arel_column
|
40
|
+
arel_table[column_name]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
# Repository class for tags.
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# repo = PgTagsOn::Repository.new(Entity, :tags)
|
8
|
+
# repo.all
|
9
|
+
# repo.update('foo', 'boo')
|
10
|
+
# ...
|
11
|
+
#
|
12
|
+
class Repository
|
13
|
+
extend Forwardable
|
14
|
+
def_delegators :gateway, :all, :all_with_counts, :taggings, :count, :create, :update, :delete
|
15
|
+
|
16
|
+
attr_reader :klass, :column_name
|
17
|
+
|
18
|
+
def initialize(klass, column_name)
|
19
|
+
@klass = klass
|
20
|
+
@column_name = column_name.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def gateway
|
26
|
+
raise 'Invalid column type' unless column.array?
|
27
|
+
|
28
|
+
@gateway ||= send("#{column.type}_gateway")
|
29
|
+
end
|
30
|
+
|
31
|
+
def column
|
32
|
+
@column ||= klass.columns_hash[column_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_gateway
|
36
|
+
PgTagsOn::Repositories::ArrayRepository.new(klass, column_name, settings)
|
37
|
+
end
|
38
|
+
|
39
|
+
def string_gateway
|
40
|
+
default_gateway
|
41
|
+
end
|
42
|
+
|
43
|
+
def text_gateway
|
44
|
+
default_gateway
|
45
|
+
end
|
46
|
+
|
47
|
+
def integer_gateway
|
48
|
+
default_gateway
|
49
|
+
end
|
50
|
+
|
51
|
+
def jsonb_gateway
|
52
|
+
PgTagsOn::Repositories::ArrayJsonbRepository.new(klass, column_name, settings)
|
53
|
+
end
|
54
|
+
|
55
|
+
def settings
|
56
|
+
@settings ||= (klass.pg_tags_on_options_for(column_name) || {}).symbolize_keys
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
# Model for tags.
|
5
|
+
# Schema is defined dynamically and has only +name+ column as string.
|
6
|
+
class Tag < ::ActiveRecord::Base
|
7
|
+
self.table_name = 'tags'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def load_schema!
|
11
|
+
@load_schema ||= begin
|
12
|
+
name_column = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Column.new('name', '', pg_string_type)
|
13
|
+
@columns_hash = { 'name' => name_column }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def pg_string_type
|
20
|
+
string = ::ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: 'character varying', type: 'string')
|
21
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQL::TypeMetadata.new(string)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
info = attributes.map { |name, value| %(#{name}: #{format_for_inspect(value)}) }.join(', ')
|
27
|
+
|
28
|
+
"#<#{self.class.name} #{info}>"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
# Helper class to construct queries.
|
5
|
+
# This class is registered in models' predicate builders.
|
6
|
+
# See configuration in order to create an alias for it.
|
7
|
+
class TagsQuery
|
8
|
+
%w[one all any in eq].each do |predicate|
|
9
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
10
|
+
def #{predicate}(*args)
|
11
|
+
params = args.size == 1 ? args.first : args
|
12
|
+
new(params, "#{predicate}")
|
13
|
+
end
|
14
|
+
RUBY
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :value
|
18
|
+
attr_reader :predicate
|
19
|
+
attr_reader :options
|
20
|
+
|
21
|
+
def initialize(value, predicate, options = {})
|
22
|
+
@value = value
|
23
|
+
@predicate = predicate
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgTagsOn
|
4
|
+
# Validator for max. number of tags and max. tag length.
|
5
|
+
#
|
6
|
+
# class Entity
|
7
|
+
# pg_tags_on :tags, limit: 20, tag_length: 64
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
class TagsValidator < ActiveModel::EachValidator
|
11
|
+
def initialize(options = {})
|
12
|
+
super
|
13
|
+
@klass = options[:class]
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate_each(record, attribute, value)
|
17
|
+
validate_limit(record, attribute, value)
|
18
|
+
validate_tag_length(record, attribute, value)
|
19
|
+
|
20
|
+
record.errors.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :klass
|
26
|
+
|
27
|
+
def validate_limit(record, attr, value)
|
28
|
+
limit = klass.pg_tags_on_options_for(attr)[:limit]
|
29
|
+
return true unless limit && value
|
30
|
+
|
31
|
+
record.errors.add(attr, "size exceeded #{limit} tags") if value.size > limit.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_tag_length(record, attr, value)
|
35
|
+
limit, key = klass.pg_tags_on_options_for(attr).values_at(:tag_length, :key)
|
36
|
+
return true unless limit && value
|
37
|
+
|
38
|
+
value.map! { |tag| tag.with_indifferent_access.dig(*key) } if key
|
39
|
+
|
40
|
+
record.errors.add(attr, "length exceeded #{limit} characters") if value.any? { |val| val.size > limit.to_i }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/pg_tags_on.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require 'pg_tags_on/version'
|
6
|
+
require 'pg_tags_on/active_record/base'
|
7
|
+
require 'pg_tags_on/active_record/arel'
|
8
|
+
require 'pg_tags_on/predicate_handler'
|
9
|
+
require 'pg_tags_on/predicate_handler/base_handler'
|
10
|
+
require 'pg_tags_on/predicate_handler/array_string_handler'
|
11
|
+
require 'pg_tags_on/predicate_handler/array_text_handler'
|
12
|
+
require 'pg_tags_on/predicate_handler/array_integer_handler'
|
13
|
+
require 'pg_tags_on/predicate_handler/array_jsonb_handler'
|
14
|
+
require 'pg_tags_on/predicate_handler/array_jsonb_with_attrs_handler'
|
15
|
+
require 'pg_tags_on/tag'
|
16
|
+
require 'pg_tags_on/tags_query'
|
17
|
+
require 'pg_tags_on/validations/validator'
|
18
|
+
require 'pg_tags_on/repository'
|
19
|
+
require 'pg_tags_on/repositories/base_repository'
|
20
|
+
require 'pg_tags_on/repositories/array_repository'
|
21
|
+
require 'pg_tags_on/repositories/array_jsonb_repository'
|
22
|
+
require 'pg_tags_on/benchmark/benchmark'
|
23
|
+
|
24
|
+
# PgTagsOn configuration methods
|
25
|
+
module PgTagsOn
|
26
|
+
class Error < StandardError; end
|
27
|
+
class ColumnNotFoundError < Error; end
|
28
|
+
|
29
|
+
def configure
|
30
|
+
@config ||= OpenStruct.new(query_class: 'Tags')
|
31
|
+
yield @config if block_given?
|
32
|
+
@config
|
33
|
+
end
|
34
|
+
|
35
|
+
def configuration
|
36
|
+
@config || configure
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_query_class
|
40
|
+
return true if query_class?
|
41
|
+
|
42
|
+
Kernel.const_set(PgTagsOn.configuration.query_class.to_sym, PgTagsOn::TagsQuery)
|
43
|
+
end
|
44
|
+
|
45
|
+
def query_class?
|
46
|
+
Kernel.const_defined?(PgTagsOn.configuration.query_class)
|
47
|
+
end
|
48
|
+
|
49
|
+
def query_class
|
50
|
+
Kernel.const_get(PgTagsOn.configuration.query_class)
|
51
|
+
end
|
52
|
+
|
53
|
+
module_function :configure, :configuration, :register_query_class, :query_class, :query_class?
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveRecord::Base.include PgTagsOn::ActiveRecord::Base
|