active_any 0.0.1 → 0.0.2

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 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