pg_tags_on 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|