active_any 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fe7ee5723d9362864d198ce08fea398a304bed1e
4
- data.tar.gz: f3bd88f1d456569eeed90e9471336a0c63f65ceb
3
+ metadata.gz: 7c70ba8be173563a70f75ae70628ddddb8dc66b9
4
+ data.tar.gz: 3b21c6608095ba05c84b2ab1256771434a567a75
5
5
  SHA512:
6
- metadata.gz: c429d20676d4b11e0ff92d22ffbea558198b148ed871c6ab2be531bb374b00b96f214c88ee54e5a812d836823aec646987a1ec847a3409a085187279658f15ca
7
- data.tar.gz: 2ef9773b72a9063711f83615dedf345457f08146f839afefd160bdbbedb2d741b85784051c7d462527e04038e5170bbb2848cdf5b882d52203faeeb99e16e50c
6
+ metadata.gz: 7e7c7311ed6633a140b529ce3acdadd7858e728322f15707c45a93a2980d14f271b3fd23e8f385604a1f9a3512dea1451ff00c93037d31c172762e3912c9951a
7
+ data.tar.gz: d3eccc65ad03a3b637bfd49ceb52f87702a36ffc58e5b74b3c11cadf65085f2ee381b671b6fba517e01f582589b185d5367f61449aabbd20ba9d350c942a4992
data/.travis.yml CHANGED
@@ -1,5 +1,18 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4.1
5
- before_install: gem install bundler -v 1.15.1
4
+ - 2.2.7
5
+ - 2.3.4
6
+ - 2.4.1
7
+ before_install:
8
+ - gem update --system
9
+ - gem install bundler -v 1.15.3
10
+ cache: bundler
11
+ script:
12
+ - bundle exec rubocop
13
+ - bundle exec rake test
14
+ notifications:
15
+ slack:
16
+ secure: S1W/Lw+dH3wb8FfkMIWPZmr6M4Q6S2WMkSlanpKva1HM7K5QL7hXdmUl2yBUxJE26BHSsb1ScozMEzadyda2+i/W34UvZ7LXgKeHkUKEdjy/AmsSJPK1ZjMfgnv10tVgbEIusNb4bF/sSuChdZKK3ILwOlqIPDlQNdMwF1xRA2xt5J7tb26UgyIzoCI4P3bJYMULWsEkk+UwHiJH0YO9ulkTZI/j0N+hLXQLJTZPjmKtMk/tE0NbBmFVL4md89hUcR5gKTGGrNzEMJ58K+zqeDG/DubkcIbA5ZuqKv+oE5m0pDODZExxnC+oeENTvq/VfYwOfD0pTDrBNYjj+Bm3YiyGDzQAgov9XPDG8g/fKEs/LNAT79UZXkZlFO99Yn/vrYH9o5DKpOE9smENUXylb55MgLTUiYe17CTp7pB3trbJl3wwIbLjSmTjAdSUNgPv8qDP4uk3K4U32mknXCDDkU9EI7f6F731ocdoxsGarEBcPcgjs73Y84iwDteQp847Gigtgo4Y4TCWH657uzLolR2O8NSw+vWT0VNI9qtR5PZD7iVYtSp1qHtPAKowCztodewY2Nu+Ds9Z95udf4GPUkFg/SNEJPTPrQFLiiJZ8UYP8NJEuA+IP1tc2zG3zU/ADrjenRC1ZiupQG7OMH82y11408U6PcHFSlF+7NuDkac=
17
+ on_success: change
18
+ on_failure: always
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # ActiveAny
2
2
 
3
+ [![Build Status](https://travis-ci.org/yuemori/active_any.svg?branch=master)](https://travis-ci.org/yuemori/active_any)
4
+
3
5
  A utility for quering interface to any objects like ActiveRecord. This gem support for querying, association and relation.
4
6
 
5
- This gem is not stable version. Be careful.
7
+ **This gem is not stable version. Be careful.**
6
8
 
7
9
  ## Installation
8
10
 
@@ -46,8 +48,8 @@ end
46
48
 
47
49
  User.all #=> Relation
48
50
  User.all.to_a #=> Array of User
49
- User.where(name: 'alice') #=> WHERE name equals alice
50
- User.where(name: /li/) #=> WHERE name LIKE li (alice, charlie)
51
+ User.where(name: 'alice') #=> WHERE name = alice
52
+ User.where(name: /li/) #=> WHERE name LIKE %li% (alice, charlie)
51
53
  User.group(:name) #=> GROUP BY name
52
54
  User.order(:age) #=> ORDER BY age
53
55
  User.order(age: :desc) #=> ORDER BY age DESC
@@ -61,15 +63,24 @@ Clone repository and run `bin/console`, if you want to try it!
61
63
 
62
64
  ## Features
63
65
 
64
- - more powerful interface for select query
65
- - [] `joins` (interface only)
66
- - [] `includes`
67
- - [] `having`
68
- - [] Interface for other than select query (create, destroy, update)
69
- - [] enum support
70
- - [] logging
71
- - [] scoping
72
- - [] ActiveRecord integration
66
+ - More powerful interface for select query
67
+ - [ ] `joins` (interface only)
68
+ - [ ] `preload` (interface only)
69
+ - [ ] `eager_load` (interface only)
70
+ - [x] `includes`
71
+ - [ ] `having`
72
+ - [ ] `offset`
73
+ - Adapters
74
+ - [ ] YAML
75
+ - [ ] JSON
76
+ - [ ] CSV
77
+ - ActiveRecord like interfaces
78
+ - [ ] Interface for other than select query (create, destroy, update)
79
+ - [ ] enum support
80
+ - [x] subscriber
81
+ - [ ] scoping
82
+ - Integration
83
+ - [ ] ActiveRecord
73
84
 
74
85
  ## Testing
75
86
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << 'test'
8
8
  t.libs << 'lib'
9
- t.test_files = FileList['test/**/*_test.rb']
9
+ t.test_files = FileList['test/**/test_*.rb']
10
10
  end
11
11
 
12
12
  task default: :test
@@ -11,17 +11,5 @@ module ActiveAny
11
11
  def query(_where_clause, _limit_value = nil, _group_values = [], _order_values = [])
12
12
  raise NotImplementedError.new, "#{self.class.name} can not handle for #{value.class}"
13
13
  end
14
-
15
- def limit_handler(_records, _limit_value)
16
- raise NotImplementedError.new, "#{self.class.name} can not handle for limit"
17
- end
18
-
19
- def group_handler(_records, _group_values)
20
- raise NotImplementedError.new, "#{self.class.name} can not handle for group"
21
- end
22
-
23
- def order_handler(_records, _order_values)
24
- raise NotImplementedError.new, "#{self.class.name} can not handle for order"
25
- end
26
14
  end
27
15
  end
@@ -3,7 +3,7 @@
3
3
  module ActiveAny
4
4
  module Associations
5
5
  class Association
6
- attr_reader :loaded, :reflection, :target, :owner
6
+ attr_reader :loaded, :reflection, :target, :owner, :inversed
7
7
 
8
8
  def initialize(owner, reflection)
9
9
  @owner = owner
@@ -61,6 +61,7 @@ module ActiveAny
61
61
  def reset
62
62
  @loaded = false
63
63
  @stale_target = nil
64
+ @inversed = false
64
65
  end
65
66
 
66
67
  def reset_scope
@@ -71,12 +72,17 @@ module ActiveAny
71
72
  @loaded
72
73
  end
73
74
 
75
+ def find_from_target?
76
+ loaded?
77
+ end
78
+
74
79
  def find_target
75
80
  raise NotImplementedError
76
81
  end
77
82
 
78
83
  def loaded!
79
84
  @loaded = true
85
+ @inversed = false
80
86
  end
81
87
 
82
88
  def target=(target)
@@ -84,11 +90,32 @@ module ActiveAny
84
90
  loaded!
85
91
  end
86
92
 
93
+ def set_inverse_instance(record)
94
+ if invertible_for?(record)
95
+ inverse = record.association(inverse_reflection_for(record).name)
96
+ inverse.target = owner
97
+ inverse.inversed = true
98
+ end
99
+ record
100
+ end
101
+
87
102
  private
88
103
 
104
+ def invertible_for?(record)
105
+ foreign_key_for?(record) && inverse_reflection_for(record)
106
+ end
107
+
108
+ def foreign_key_for?(record)
109
+ record.has_attribute?(reflection.foreign_key)
110
+ end
111
+
89
112
  def find_target?
90
113
  !loaded? && klass
91
114
  end
115
+
116
+ def inverse_reflection_for(_record)
117
+ reflection.inverse_of
118
+ end
92
119
  end
93
120
  end
94
121
  end
@@ -15,6 +15,11 @@ module ActiveAny
15
15
  raise NotImplementedError.new, 'writer is unimplemented'
16
16
  end
17
17
 
18
+ def reset
19
+ super
20
+ @target = []
21
+ end
22
+
18
23
  def size
19
24
  if !find_target? || loaded?
20
25
  target.size
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_any/associations/preloader/association'
4
+
5
+ module ActiveAny
6
+ module Associations
7
+ class Preloader
8
+ class Association
9
+ attr_reader :reflection, :preload_scope, :owners, :klass, :preloaded_records
10
+
11
+ def initialize(klass, owner_records, reflection, preload_scope)
12
+ @klass = klass
13
+ @owners = owner_records
14
+ @reflection = reflection
15
+ @preload_scope = preload_scope
16
+ @preloaded_records = []
17
+ end
18
+
19
+ def run(preloader)
20
+ preload(preloader)
21
+ end
22
+
23
+ def preload(_preloader)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def scope
28
+ @scope ||= build_scope
29
+ end
30
+
31
+ def records_for(ids)
32
+ scope.where(association_key_name => ids)
33
+ end
34
+
35
+ # The name of the key on the associated records
36
+ def association_key_name
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def owner_key_name
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def options
45
+ reflection.options
46
+ end
47
+
48
+ private
49
+
50
+ def associated_records_by_owner(_preloader)
51
+ records = load_records do |record|
52
+ owner = owners_by_key[record[association_key_name]]
53
+ association = owner.association(reflection.name)
54
+ association.set_inverse_instance(record)
55
+ end
56
+
57
+ owners.each_with_object({}) do |owner, result|
58
+ result[owner] = records[owner[owner_key_name]] || []
59
+ end
60
+ end
61
+
62
+ def owners_by_key
63
+ @owners_by_key ||= owners.each_with_object({}) do |owner, h|
64
+ h[owner[owner_key_name]] = owner
65
+ end
66
+ end
67
+
68
+ def owner_keys
69
+ @owner_keys ||= owners.map do |owner|
70
+ owner[owner_key_name]
71
+ end.uniq.compact
72
+ end
73
+
74
+ def load_records
75
+ return {} if owner_keys.empty?
76
+ @preloaded_records = records_for(owner_keys).load
77
+ @preloaded_records.each do |record|
78
+ yield record if block_given?
79
+ end
80
+ @preloaded_records.group_by do |record|
81
+ record[association_key_name]
82
+ end
83
+ end
84
+
85
+ def reflection_scope
86
+ @reflection_scope ||= reflection.scope_for(klass)
87
+ end
88
+
89
+ def build_scope # rubocop:disable all
90
+ scope = klass.unscoped
91
+
92
+ values = reflection_scope.values
93
+ preload_values = preload_scope.values
94
+
95
+ scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
96
+ # scope.references_values = Array(values[:references]) + Array(preload_values[:references])
97
+
98
+ if preload_values[:select] || values[:select]
99
+ scope._select!(preload_values[:select] || values[:select])
100
+ end
101
+ scope.includes! preload_values[:includes] || values[:includes]
102
+ # if preload_scope.joins_values.any?
103
+ # scope.joins!(preload_scope.joins_values)
104
+ # else
105
+ # scope.joins!(reflection_scope.joins_values)
106
+ # end
107
+ order_values = preload_values[:order] || values[:order]
108
+ if order_values
109
+ scope.order!(order_values)
110
+ end
111
+
112
+ # if preload_values[:reordering] || values[:reordering]
113
+ # scope.reordering_value = true
114
+ # end
115
+
116
+ # if preload_values[:readonly] || values[:readonly]
117
+ # scope.readonly!
118
+ # end
119
+
120
+ # if options[:as]
121
+ # scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
122
+ # end
123
+
124
+ # scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
125
+ klass.default_scoped.merge(scope)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAny
4
+ module Associations
5
+ class Preloader
6
+ class BelongsTo < Association
7
+ def preload(preloader)
8
+ associated_records_by_owner(preloader).each do |owner, associated_records|
9
+ record = associated_records.first
10
+
11
+ association = owner.association(reflection.name)
12
+ association.target = record
13
+ end
14
+ end
15
+
16
+ def association_key_name
17
+ reflection.options[:primary_key] || klass && klass.primary_key
18
+ end
19
+
20
+ def owner_key_name
21
+ reflection.foreign_key
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAny
4
+ module Associations
5
+ class Preloader
6
+ class HasMany < Association
7
+ def preload(preloader)
8
+ associated_records_by_owner(preloader).each do |owner, records|
9
+ association = owner.association(reflection.name)
10
+ association.loaded!
11
+ association.target.concat(records)
12
+ end
13
+ end
14
+
15
+ def association_key_name
16
+ reflection.foreign_key
17
+ end
18
+
19
+ def owner_key_name
20
+ reflection.primary_key
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_any/associations/preloader/association'
4
+ require 'active_any/associations/preloader/has_many'
5
+ require 'active_any/associations/preloader/belongs_to'
6
+
7
+ module ActiveAny
8
+ module Associations
9
+ class Preloader
10
+ NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
11
+
12
+ def preload(records, associations, scope = NULL_RELATION)
13
+ records = Array.wrap(records).compact.uniq
14
+ associations = Array.wrap(associations)
15
+
16
+ if records.empty?
17
+ []
18
+ else
19
+ associations.flat_map do |association|
20
+ preloaders_on association, records, scope
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def preloaders_on(association, records, scope)
28
+ case association
29
+ when Symbol
30
+ preloaders_for_one(association, records, scope)
31
+ when String
32
+ preloaders_for_one(association.to_sym, records, scope)
33
+ else
34
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
35
+ end
36
+ end
37
+
38
+ def preloaders_for_one(association, records, scope)
39
+ grouped_records(association, records).flat_map do |reflection, klasses|
40
+ klasses.map do |rhs_klass, rs|
41
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
42
+ loader.run self
43
+ loader
44
+ end
45
+ end
46
+ end
47
+
48
+ def grouped_records(association, records)
49
+ h = {}
50
+ records.each do |record|
51
+ next unless record
52
+ assoc = record.association(association)
53
+ klasses = h[assoc.reflection] ||= {}
54
+ (klasses[assoc.klass] ||= []) << record
55
+ end
56
+ h
57
+ end
58
+
59
+ class AlreadyLoaded
60
+ attr_reader :owners, :reflection
61
+
62
+ def initialize(_klass, owners, reflection, _preload_scope)
63
+ @owners = owners
64
+ @reflection = reflection
65
+ end
66
+
67
+ def run(preloader); end
68
+
69
+ def preloaded_records
70
+ owners.flat_map { |owner| owner.association(reflection.name).target }
71
+ end
72
+ end
73
+
74
+ class NullPreloader
75
+ def self.new(_klass, _owners, _reflection, _preload_scope)
76
+ self
77
+ end
78
+
79
+ def self.run(_preloader); end
80
+
81
+ def self.preloaded_records
82
+ []
83
+ end
84
+
85
+ def self.owners
86
+ []
87
+ end
88
+ end
89
+
90
+ def preloader_for(reflection, owners, rhs_klass) # rubocop:disable Metrics/MethodLength
91
+ return NullPreloader unless rhs_klass
92
+
93
+ if owners.first.association(reflection.name).loaded?
94
+ return AlreadyLoaded
95
+ end
96
+ reflection.check_preloadable!
97
+
98
+ case reflection.macro
99
+ when :has_many
100
+ # reflection.options[:through] ? HasManyThrough : HasMany
101
+ HasMany
102
+ when :has_one
103
+ # reflection.options[:through] ? HasOneThrough : HasOne
104
+ HasOne
105
+ when :belongs_to
106
+ BelongsTo
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_any/associations/preloader'
3
4
  require 'active_any/associations/collection_proxy'
4
5
  require 'active_any/associations/association_scope'
5
6
  require 'active_any/associations/builder/association'
@@ -40,13 +41,6 @@ module ActiveAny
40
41
  super
41
42
  end
42
43
 
43
- private
44
-
45
- def init_internals(*)
46
- @association_cache = {}
47
- super
48
- end
49
-
50
44
  def association(name)
51
45
  association = association_instance_get(name)
52
46
 
@@ -61,6 +55,13 @@ module ActiveAny
61
55
  association
62
56
  end
63
57
 
58
+ private
59
+
60
+ def init_internals(*)
61
+ @association_cache = {}
62
+ super
63
+ end
64
+
64
65
  def association_instance_get(name)
65
66
  @association_cache[name]
66
67
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAny
4
+ module Attribute
5
+ extend ActiveSupport::Concern
6
+
7
+ def attributes
8
+ @attributes ||= {}
9
+ end
10
+
11
+ def has_attribute?(attribute)
12
+ attributes.key?(attribute)
13
+ end
14
+
15
+ def read_attribute(name)
16
+ attributes.fetch(name)
17
+ end
18
+
19
+ def attribute_for_inspect(attr_name)
20
+ value = read_attribute(attr_name)
21
+
22
+ if value.is_a?(String) && value.length > 50
23
+ "#{value[0, 50]}...".inspect
24
+ elsif value.is_a?(Date) || value.is_a?(Time)
25
+ %("#{value.to_s(:db)}")
26
+ else
27
+ value.inspect
28
+ end
29
+ end
30
+
31
+ def inspect
32
+ inspection = self.class.attribute_names.collect do |name|
33
+ "#{name}: #{attribute_for_inspect(name)}" if has_attribute?(name)
34
+ end.compact.join(', ')
35
+
36
+ "#<#{self.class} #{inspection}>"
37
+ end
38
+
39
+ module ClassMethods
40
+ def attributes(*names)
41
+ names.each { |name| attribute name }
42
+ end
43
+
44
+ def attribute(name)
45
+ attribute_names << name.to_sym
46
+ define_writer_method name
47
+ define_reader_method name
48
+ end
49
+
50
+ def attribute_names
51
+ @attribute_names ||= []
52
+ end
53
+
54
+ def define_writer_method(name)
55
+ define_method "#{name}=" do |value|
56
+ attributes[name] = value
57
+ end
58
+ end
59
+
60
+ def define_reader_method(name)
61
+ define_method name do
62
+ attributes.fetch(name)
63
+ end
64
+ end
65
+
66
+ def has_attribute?(attribute)
67
+ attribute_names.key?(attribute)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAny
4
+ class Base
5
+ include Core
6
+ include Attribute
7
+ include Associations
8
+ include AttributeAssignment
9
+ include Finders
10
+ include Reflection
11
+ end
12
+
13
+ class Object < Base
14
+ def self.inherited(child)
15
+ child.abstract_class = false
16
+ end
17
+
18
+ class << self
19
+ attr_accessor :data
20
+
21
+ def adapter
22
+ @adapter ||= ObjectAdapter.new(self)
23
+ end
24
+ end
25
+ end
26
+
27
+ class Hash < Base
28
+ def self.inherited(child)
29
+ child.abstract_class = false
30
+ end
31
+
32
+ class << self
33
+ attr_reader :data
34
+
35
+ def data=(data)
36
+ data.map(&:keys).flatten.uniq.each do |method|
37
+ attribute method
38
+ end
39
+
40
+ @data = data.map { |d| new(d) }
41
+ end
42
+
43
+ def adapter
44
+ @adapter ||= ObjectAdapter.new(self)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAny
4
+ class Configuration
5
+ attr_accessor :logger, :log_level
6
+
7
+ def logger
8
+ @logger ||= begin
9
+ logger = Logger.new($stdout)
10
+ logger.level = "::Logger::#{log_level.to_s.upcase}".constantize
11
+ logger
12
+ end
13
+ end
14
+
15
+ def log_level
16
+ @log_level ||= :debug
17
+ end
18
+ end
19
+ end
@@ -12,13 +12,29 @@ module ActiveAny
12
12
  end
13
13
 
14
14
  class_methods do
15
- delegate :take, :take!, :first, :last, to: :all
15
+ delegate :includes, :take, :take!, :first, :last, to: :all
16
+ end
17
+
18
+ module ClassMethods
19
+ def unscoped
20
+ # TODO: implement
21
+ all
22
+ end
23
+
24
+ def default_scoped
25
+ # TODO: implement
26
+ all
27
+ end
16
28
  end
17
29
 
18
30
  def initialize(*args)
19
31
  init_internals(*args)
20
32
  end
21
33
 
34
+ def [](key)
35
+ send(key)
36
+ end
37
+
22
38
  private
23
39
 
24
40
  def init_internals(*); end