any_query 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.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyQuery
4
+ # @api private
5
+ class Query
6
+ include Enumerable
7
+
8
+ delegate_missing_to :to_a
9
+
10
+ def initialize(model, adapter)
11
+ @model = model
12
+ @adapter = adapter
13
+ end
14
+
15
+ def joins(model, primary_key, foreign_key, into:, as: :single, strategy: :default)
16
+ dup.joins!(model, primary_key, foreign_key, into:, as:, strategy:)
17
+ end
18
+
19
+ def with_single
20
+ dup.joins!(:show, :id, :id, into: :single, as: :single, strategy: :single)
21
+ end
22
+
23
+ def where(options)
24
+ dup.where!(options)
25
+ end
26
+
27
+ def select(*args)
28
+ dup.select!(*args)
29
+ end
30
+
31
+ def limit(limit)
32
+ dup.limit!(limit)
33
+ end
34
+
35
+ def find(id)
36
+ @adapter.load_single(@model, id, [])
37
+ end
38
+
39
+ def to_a
40
+ @adapter.load(@model, select: @select, joins: @joins, where: @where, limit: @limit)
41
+ end
42
+
43
+ def each(&block)
44
+ to_a.each(&block)
45
+ end
46
+
47
+ def joins!(model, primary_key, foreign_key, into:, as: :single, strategy: :default)
48
+ @joins ||= []
49
+ @joins << ({ model:, primary_key:, foreign_key:, into:, as:, strategy: })
50
+ self
51
+ end
52
+
53
+ def select!(*args)
54
+ @select ||= []
55
+ @select += args
56
+ self
57
+ end
58
+
59
+ def where!(options)
60
+ @where ||= []
61
+ @where << options
62
+ self
63
+ end
64
+
65
+ def limit!(limit)
66
+ @limit = limit
67
+ self
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyQuery
4
+ VERSION = '0.1.1'
5
+ end
data/lib/any_query.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'active_support/all'
5
+ require 'active_record'
6
+ require 'active_model'
7
+
8
+ # AnyQuery is a library to help you build queries for different data sources
9
+ module AnyQuery
10
+ extend ActiveSupport::Concern
11
+ extend ActiveSupport::Autoload
12
+
13
+ autoload :Config
14
+ autoload :Adapters
15
+ autoload :Query
16
+
17
+ included do
18
+ delegate_missing_to :@attributes
19
+ end
20
+
21
+ def initialize
22
+ @attributes = OpenStruct.new
23
+ end
24
+
25
+ module ClassMethods
26
+ def adapter(name, &block)
27
+ config = "AnyQuery::Adapters::#{name.to_s.classify}::Config".constantize.new(&block)
28
+ @adapter = "AnyQuery::Adapters::#{name.to_s.classify}".constantize.new(config)
29
+ end
30
+
31
+ delegate_missing_to :all
32
+
33
+ # @return [AnyQuery::Adapters::Base]
34
+ def _adapter
35
+ @adapter
36
+ end
37
+
38
+ # @param [Symbol] name
39
+ # @param [Hash] options
40
+ # @option options [Symbol] :type
41
+ # @option options [String] :format
42
+ # @option options [Integer] :length
43
+ # @option options [Proc] :transform
44
+ def field(name, options = {})
45
+ fields[name] = options
46
+ end
47
+
48
+ # @return [Hash]
49
+ def fields
50
+ @fields ||= {}
51
+ end
52
+
53
+ # @return [AnyQuery::Query]
54
+ def all
55
+ Query.new(self, @adapter)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ArticleCSV do
6
+ it 'returns records' do
7
+ expect(described_class.all.to_a).to have(2).items
8
+ end
9
+
10
+ it 'can be finded' do
11
+ result = described_class.find(2)
12
+ expect(result.id).to eq(2)
13
+ expect(result.user_id).to eq(1)
14
+ expect(result.title).to eq('the article 2')
15
+ expect(result.body).to eq('this is the body of your article 2')
16
+ expect(result.status).to eq(2)
17
+ expect(result.created_at).to eq('2023-03-10T18:28:02Z'.to_datetime)
18
+ end
19
+
20
+ it 'can be filtered' do
21
+ expect(described_class.where(status: 1).to_a).to have(1).items
22
+ end
23
+
24
+ it 'can be limited' do
25
+ expect(described_class.limit(1).to_a).to have(1).items
26
+ end
27
+
28
+ it 'can be joined' do
29
+ expect do
30
+ described_class.joins(UserSQL, :id, :user_id, into: :user).to_a.first
31
+ end.to match_query(/SELECT "users".* FROM "users"/)
32
+ end
33
+
34
+ it 'can be selected with nested selectors' do
35
+ results = described_class
36
+ .joins(UserSQL, :id, :user_id, into: :user, as: :single)
37
+ .select(:id, :title, %i[user email])
38
+ .to_a
39
+
40
+ expect(results).to eq(
41
+ [[1, 'the article', 'test@test.com'], [2, 'the article 2', 'test@test.com']]
42
+ )
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ArticleFL do
6
+ it 'returns records' do
7
+ expect(described_class.all.to_a).to have(2).items
8
+ end
9
+
10
+ it 'can be finded' do
11
+ result = described_class.find(2)
12
+ expect(result.id).to eq(2)
13
+ expect(result.user_id).to eq(1)
14
+ expect(result.title).to eq('this is another sample')
15
+ expect(result.body).to eq('this is an example of a body for an article that is very long and dirty')
16
+ expect(result.status).to eq(2)
17
+ expect(result.created_at).to eq('2023-12-31T19:00:00Z'.to_datetime)
18
+ end
19
+
20
+ it 'can be filtered' do
21
+ expect(described_class.where(status: 1).to_a).to have(1).items
22
+ end
23
+
24
+ it 'can be limited' do
25
+ expect(described_class.limit(1).to_a).to have(1).items
26
+ end
27
+
28
+ it 'can be joined' do
29
+ expect do
30
+ described_class.joins(UserSQL, :id, :user_id, into: :user).to_a.first
31
+ end.to match_query(/SELECT "users".* FROM "users"/)
32
+ end
33
+
34
+ it 'can be selected with nested selectors' do
35
+ results = described_class
36
+ .joins(UserSQL, :id, :user_id, into: :user, as: :single)
37
+ .select(:id, :title, %i[user email])
38
+ .to_a
39
+
40
+ expect(results).to eq(
41
+ [
42
+ [1, 'this is a sample', 'test@test.com'],
43
+ [2, 'this is another sample', 'test@test.com']
44
+ ]
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ArticleHTTP do
6
+ before do
7
+ stub_request(:get, 'http://example.com/articles?page=0&some_query_param=1')
8
+ .to_return(
9
+ headers: { content_type: 'application/json' },
10
+ body: JSON.dump(
11
+ {
12
+ items: [
13
+ {
14
+ id: 1,
15
+ user_id: 1,
16
+ name: 'some article'
17
+ },
18
+ {
19
+ id: 2,
20
+ user_id: 1,
21
+ name: 'some article 2'
22
+ }
23
+ ]
24
+ }
25
+ )
26
+ )
27
+
28
+ stub_request(:get, 'http://example.com/articles?page=1&some_query_param=1')
29
+ .to_return(
30
+ headers: { content_type: 'application/json' },
31
+ body: '{ "items": [] }'
32
+ )
33
+ end
34
+
35
+ it 'returns records' do
36
+ expect(described_class.all.to_a).to have(2).items
37
+ end
38
+
39
+ it 'can be finded' do
40
+ stub_request(:get, 'http://example.com/articles/1')
41
+ .to_return(
42
+ headers: { content_type: 'application/json' },
43
+ body: JSON.dump(
44
+ {
45
+ id: 1,
46
+ user_id: 1,
47
+ title: 'some article'
48
+ }
49
+ )
50
+ )
51
+ result = described_class.find(1)
52
+ expect(result.id).to eq(1)
53
+ expect(result.user_id).to eq(1)
54
+ expect(result.title).to eq('some article')
55
+ end
56
+
57
+ it 'can be filtered' do
58
+ stub_request(:get, 'http://example.com/articles?page=0&some_query_param=1&status=1')
59
+ .to_return(
60
+ headers: { content_type: 'application/json' },
61
+ body: JSON.dump(
62
+ {
63
+ items: [
64
+ {
65
+ id: 1,
66
+ name: 'some article'
67
+ }
68
+ ]
69
+ }
70
+ )
71
+ )
72
+ stub_request(:get, 'http://example.com/articles?page=1&some_query_param=1&status=1')
73
+ .to_return(
74
+ headers: { content_type: 'application/json' },
75
+ body: '{ "items": [] }'
76
+ )
77
+
78
+ expect(described_class.where(status: 1).to_a).to have(1).items
79
+ end
80
+
81
+ it 'can be limited' do
82
+ expect(described_class.limit(1).to_a).to have(1).items
83
+ end
84
+
85
+ it 'can be joined with SQL' do
86
+ described_class.joins(UserSQL, :id, :user_id, into: :user).to_a.first
87
+ end
88
+
89
+ it 'can be joined with itself using the show endpoint' do
90
+ stub_request(:get, 'http://example.com/articles/1')
91
+ .to_return(
92
+ headers: { content_type: 'application/json' },
93
+ body: JSON.dump(
94
+ {
95
+ id: 1,
96
+ user_id: 1,
97
+ title: 'some article',
98
+ some_additional_field: 'some value'
99
+ }
100
+ )
101
+ )
102
+
103
+ stub_request(:get, 'http://example.com/articles/2')
104
+ .to_return(
105
+ headers: { content_type: 'application/json' },
106
+ body: JSON.dump(
107
+ {
108
+ id: 2,
109
+ user_id: 1,
110
+ title: 'some article 2',
111
+ some_additional_field: 'some value'
112
+ }
113
+ )
114
+ )
115
+
116
+ result = described_class.with_single.to_a.first
117
+ expect(result.some_additional_field).to eq('some value')
118
+ end
119
+
120
+ it 'can be joined with HTTP(s)' do
121
+ stub_request(:get, 'http://example.com/users?id%5B%5D=1&some_query_param=true')
122
+ .to_return(
123
+ headers: { content_type: 'application/json' },
124
+ body: JSON.dump(
125
+ items: [
126
+ { "id": 1, "email": 'gianni@gianni.com' }
127
+ ],
128
+ cursor: '123123123'
129
+ )
130
+ )
131
+
132
+ stub_request(:get, 'http://example.com/users?id%5B%5D=1&some_query_param=true&cursor=123123123')
133
+ .to_return(
134
+ headers: { content_type: 'application/json' },
135
+ body: JSON.dump(
136
+ items: [],
137
+ cursor: '123123123'
138
+ )
139
+ )
140
+
141
+ result = described_class.joins(UserHTTP, :id, :user_id, into: :user).to_a.first
142
+ expect(result.user.email).to eq('gianni@gianni.com')
143
+ end
144
+
145
+ it 'can be selected with nested selectors' do
146
+ results = described_class
147
+ .joins(UserSQL, :id, :user_id, into: :user, as: :single)
148
+ .select(:id, :title, %i[user email])
149
+ .to_a
150
+
151
+ expect(results).to have(2).items
152
+ expect(results[0]).to have(3).items
153
+ end
154
+
155
+ context 'with url params' do
156
+ it 'can be filtered using query params' do
157
+ stub_request(:get, 'http://example.com/1/users')
158
+ ScopedUserHTTP.where(company_id: 1).to_a
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ArticleSQL do
6
+ before do
7
+ ActiveRecord::Base.connection.execute('INSERT INTO articles VALUES (1, 1, "Title 1", "Body 1", 1, "2021-01-01 00:00:00", "2021-01-01 00:00:00")')
8
+ ActiveRecord::Base.connection.execute('INSERT INTO articles VALUES (2, 1, "Title 2", "Body 2", 2, "2021-01-01 00:00:00", "2021-01-01 00:00:00")')
9
+ end
10
+
11
+ it 'returns records' do
12
+ expect(described_class.all.to_a).to have(2).items
13
+ end
14
+
15
+ it 'can be finded' do
16
+ result = described_class.find(2)
17
+ expect(result.id).to eq(2)
18
+ expect(result.user_id).to eq(1)
19
+ expect(result.title).to eq('Title 2')
20
+ expect(result.body).to eq('Body 2')
21
+ expect(result.status).to eq(2)
22
+ expect(result.created_at).to eq('2021-01-01 00:00:00 UTC'.to_datetime)
23
+ end
24
+
25
+ it 'can be filtered' do
26
+ expect(described_class.where(status: 1).to_a).to have(1).items
27
+ end
28
+
29
+ it 'can be limited' do
30
+ expect(described_class.limit(1).to_a).to have(1).items
31
+ end
32
+
33
+ it 'can be joined' do
34
+ expect do
35
+ described_class.joins(UserSQL, :id, :user_id, into: :user).to_a.first
36
+ end.to match_query(/ LEFT OUTER JOIN "users"/)
37
+ end
38
+
39
+ it 'can be joined on many' do
40
+ ActiveRecord::Base.connection.execute('INSERT INTO comments VALUES (1, 1, "content")')
41
+ ActiveRecord::Base.connection.execute('INSERT INTO comments VALUES (2, 1, "content2")')
42
+
43
+ expect do
44
+ result = described_class.joins(CommentSQL, :id, :article_id, into: :comments, as: :list).to_a.first
45
+ expect(result.comments).to have(2).items
46
+ end.to match_query(/ LEFT OUTER JOIN "comments" ON "comments"/)
47
+ end
48
+
49
+ it 'can be selected with nested selectors' do
50
+ results = described_class
51
+ .joins(UserSQL, :id, :user_id, into: :user, as: :single)
52
+ .select(:id, :title, %i[user email])
53
+ .to_a
54
+
55
+ expect(results).to have(2).items
56
+ expect(results[0]).to have(3).items
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ id,user_id,title,body,status,created_at
2
+ 1,1,the article,this is the body of your article,1,2022-12-03 10:28:02
3
+ 2,1,the article 2,this is the body of your article 2,2,2023-03-10 18:28:02
@@ -0,0 +1,2 @@
1
+ 00010001this is a sample this is an example of a body for an article that is very long 120220101103822
2
+ 00020001this is another sample this is an example of a body for an article that is very long and dirty 220231231190000
@@ -0,0 +1,32 @@
1
+ # require 'rails'
2
+ # require 'action_controller/railtie'
3
+ # require 'action_mailer/railtie'
4
+ # require 'action_view/railtie'
5
+ # require 'rspec/rails'
6
+ # require 'cancancan'
7
+ # require 'active_model_serializers'
8
+ require 'any_query'
9
+ require 'rspec/collection_matchers'
10
+ require 'rspec/sql_matcher'
11
+ require 'webmock/rspec'
12
+
13
+ # I18n.enforce_available_locales = false
14
+ RSpec::Expectations.configuration.warn_about_potential_false_positives = false
15
+
16
+ # Rails.application.config.eager_load = false
17
+ # Rails.application.config.active_record.legacy_connection_handling = false
18
+
19
+ Dir[File.expand_path('support/*.rb', __dir__)].each { |f| require f }
20
+
21
+ RSpec.configure do |config|
22
+ config.before(:suite) do
23
+ Schema.create
24
+ end
25
+
26
+ config.around(:each) do |example|
27
+ ActiveRecord::Base.transaction do
28
+ example.run
29
+ raise ActiveRecord::Rollback
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ # @example
6
+ class ArticleSQL
7
+ include AnyQuery
8
+
9
+ adapter :sql do
10
+ url 'sqlite3::memory:'
11
+ primary_key :id
12
+ table 'articles'
13
+ end
14
+ end
15
+
16
+ # @example
17
+ class UserSQL
18
+ include AnyQuery
19
+
20
+ adapter :sql do
21
+ url 'sqlite3::memory:'
22
+ primary_key :id
23
+ table 'users'
24
+ end
25
+ end
26
+
27
+ # @example
28
+ class CommentSQL
29
+ include AnyQuery
30
+
31
+ adapter :sql do
32
+ url 'sqlite3::memory:'
33
+ primary_key :id
34
+ table 'comments'
35
+ end
36
+ end
37
+
38
+ class ArticleHTTP
39
+ include AnyQuery
40
+
41
+ adapter :http do
42
+ url 'http://example.com'
43
+ primary_key :id
44
+ endpoint :list, :get, "/articles",
45
+ wrapper: [:items],
46
+ pagination: { type: :page },
47
+ default_params: {
48
+ query: { some_query_param: 1 },
49
+ headers: { 'Authorization': 'some' }
50
+ }
51
+ endpoint :show, :get, "/articles/{id}"
52
+ end
53
+ end
54
+
55
+ class UserHTTP
56
+ include AnyQuery
57
+
58
+ adapter :http do
59
+ url 'http://example.com'
60
+ primary_key :id
61
+ endpoint :list, :get, "/users",
62
+ wrapper: [:items],
63
+ pagination: { type: :cursor },
64
+ default_params: {
65
+ query: { some_query_param: true },
66
+ headers: { 'Authorization': 'some' }
67
+ }
68
+ endpoint :show, :get, "/users/{id}"
69
+ end
70
+ end
71
+
72
+ class ScopedUserHTTP
73
+ include AnyQuery
74
+
75
+ adapter :http do
76
+ url 'http://example.com'
77
+ primary_key :id
78
+ endpoint :list, :get, "/{company_id}/users",
79
+ wrapper: [:items],
80
+ pagination: { type: :none },
81
+ default_params: {
82
+ headers: { 'Authorization': 'some' }
83
+ }
84
+ endpoint :show, :get, "/{company_id}/users/{id}"
85
+ end
86
+ end
87
+
88
+ class ArticleCSV
89
+ include AnyQuery
90
+
91
+ adapter :csv do
92
+ url File.join(__dir__, '../fixtures/sample.csv')
93
+ primary_key :id
94
+ end
95
+
96
+ field :id, type: :integer
97
+ field :user_id, type: :integer
98
+ field :title, type: :string
99
+ field :body, type: :string
100
+ field :status, type: :integer
101
+ field :created_at, type: :datetime, format: '%Y-%m-%d %H:%M:%S'
102
+ end
103
+
104
+ class ArticleFL
105
+ include AnyQuery
106
+
107
+ adapter :fixed_length do
108
+ url File.join(__dir__, '../fixtures/sample.txt')
109
+ primary_key :id
110
+ end
111
+
112
+ field :id, type: :integer, length: 4
113
+ field :user_id, type: :integer, length: 4
114
+ field :title, type: :string, length: 30
115
+ field :body, type: :string, length: 100
116
+ field :status, type: :integer, length: 1
117
+ field :created_at, type: :datetime, format: '%Y%m%d%H%M%S', length: 14
118
+ end
119
+
120
+ module Schema
121
+ def self.create
122
+ ActiveRecord::Migration.verbose = false
123
+
124
+ ActiveRecord::Schema.define do
125
+ create_table :users, force: true do |t|
126
+ t.string :email
127
+ t.timestamps null: false
128
+ end
129
+
130
+ create_table :articles, force: true do |t|
131
+ t.integer :user_id
132
+ t.string :title
133
+ t.text :body
134
+ t.integer :status
135
+ t.timestamps null: false
136
+ end
137
+
138
+ create_table :comments, force: true do |t|
139
+ t.integer :article_id
140
+ t.text :body
141
+ end
142
+
143
+ ActiveRecord::Base.connection.execute("INSERT INTO users VALUES (1, 'test@test.com', '2021-01-01 00:00:00', '2021-01-01 00:00:00')")
144
+ end
145
+ end
146
+ end