any_query 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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