httpsql 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +87 -4
- data/Rakefile +7 -0
- data/httpsql.gemspec +8 -6
- data/lib/httpsql.rb +37 -32
- data/lib/httpsql/version.rb +1 -1
- data/test/httpsql_test.rb +126 -0
- metadata +64 -18
- checksums.yaml +0 -7
data/README.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# Httpsql
|
2
2
|
|
3
|
-
|
3
|
+
Httpsql is a module, designed to be included in [Active Record](http://api.rubyonrails.org/classes/ActiveRecord/Base.html)
|
4
|
+
models exposed by [grape](https://github.com/intridea/grape). Once the module is
|
5
|
+
included, a given model can respond directly to query params passed to it, using
|
6
|
+
`where_params_eq`. You can also constrain the fields returned by the model,
|
7
|
+
using the `fields` query parameter.
|
8
|
+
|
9
|
+
Httpsql uses [ARel](http://www.slideshare.net/flah00/activerecord-arel) to
|
10
|
+
generate queries and exposes ARel's methods via query params. The supported ARel
|
11
|
+
methods are eq, not_eq, matches, does_not_match, gt, gteq, lt, lteq.
|
12
|
+
|
13
|
+
Httpsql also generates documentaion for endpoints, which can be easily merged
|
14
|
+
into your existing documentation (`#route_params`).
|
15
|
+
|
16
|
+
Httpsql reserves one parameter, access_token. If your model has a field called
|
17
|
+
access_token, you'll need to rename it.
|
4
18
|
|
5
19
|
## Installation
|
6
20
|
|
@@ -18,12 +32,81 @@ Or install it yourself as:
|
|
18
32
|
|
19
33
|
## Usage
|
20
34
|
|
21
|
-
|
35
|
+
Assume you have a model, widget, whose fields are id, int_field, string_field, created_at, updated_at.
|
36
|
+
|
37
|
+
create_table "widgets", :force => true do |t|
|
38
|
+
t.integer "int_field"
|
39
|
+
t.string "string_field"
|
40
|
+
t.datetime "created_at", :null => false
|
41
|
+
t.datetime "updated_at", :null => false
|
42
|
+
end
|
43
|
+
|
44
|
+
### model.rb
|
45
|
+
|
46
|
+
class Widget < ActiveRecord::Base
|
47
|
+
include Httpsql
|
48
|
+
attr_accessible :int_field, :string_field
|
49
|
+
end
|
50
|
+
|
51
|
+
### api.rb
|
52
|
+
|
53
|
+
class Api < Grape::API
|
54
|
+
version 'v1'
|
55
|
+
logger Rails.logger
|
56
|
+
default_format :json
|
57
|
+
|
58
|
+
resource :widgets do
|
59
|
+
desc 'Get all widgets', {
|
60
|
+
optional_params: Widget.route_params
|
61
|
+
}
|
62
|
+
get '/' do
|
63
|
+
present Widget.where_params_eq(params)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
### config.ru
|
69
|
+
|
70
|
+
api = Rack::Builder.new do
|
71
|
+
run Api
|
72
|
+
end
|
73
|
+
run Rack::URLMap.new \
|
74
|
+
'/' => YourApp::Application,
|
75
|
+
'/api' => api
|
76
|
+
|
77
|
+
Now you're able to run your app
|
78
|
+
|
79
|
+
rails s
|
80
|
+
|
81
|
+
Query your new API
|
82
|
+
|
83
|
+
curl 'http://localhost:3000/api/v1/widgets'
|
84
|
+
SELECT * FROM widgets
|
85
|
+
|
86
|
+
curl 'http://localhost:3000/api/v1/widgets?id=1'
|
87
|
+
SELECT * FROM widgets WHERE id = 1
|
88
|
+
|
89
|
+
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2'
|
90
|
+
SELECT * FROM widgets WHERE id IN (1,2)
|
91
|
+
|
92
|
+
curl 'http://localhost:3000/api/v1/widgets?id.gt=10&id.lt=100'
|
93
|
+
SELECT * FROM widgets WHERE id > 10 AND id < 100
|
94
|
+
|
95
|
+
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01'
|
96
|
+
SELECT * FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
|
97
|
+
|
98
|
+
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01&fields[]=id&fields[]=int_field'
|
99
|
+
SELECT id, int_field FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
|
100
|
+
|
101
|
+
curl 'http://localhost:3000/api/v1/describe_api'
|
102
|
+
Returns JSON describing the API
|
22
103
|
|
23
104
|
## Contributing
|
24
105
|
|
25
106
|
1. Fork it
|
26
107
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
108
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
-
|
29
|
-
|
109
|
+
* write some god damned tests
|
110
|
+
4. Run your god damned tests (`rake test`)
|
111
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
112
|
+
6. Create new Pull Request
|
data/Rakefile
CHANGED
data/httpsql.gemspec
CHANGED
@@ -6,11 +6,11 @@ require 'httpsql/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "httpsql"
|
8
8
|
spec.version = Httpsql::VERSION
|
9
|
-
spec.authors = ["
|
10
|
-
spec.email = ["
|
11
|
-
spec.description = %q{
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.homepage = ""
|
9
|
+
spec.authors = [ "Philip Champon", "Alejandro Ciniglio", "Sean Shillo" ]
|
10
|
+
spec.email = [ "philip@adaptly.com", "alejandro@adaptly.com", "sean@adaptly.com" ]
|
11
|
+
spec.description = %q{Expose model columns and ARel methods through query parameters in grape end points}
|
12
|
+
spec.summary = %q{Select model specified fields, create arbitrary queries, all using CGI query parameters}
|
13
|
+
spec.homepage = "https://github.com/Adaptly/httpsql"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
@@ -20,5 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
22
|
spec.add_development_dependency "rake"
|
23
|
-
spec.add_dependency "
|
23
|
+
spec.add_dependency "activerecord", ">= 3.0"
|
24
|
+
spec.add_dependency "grape"
|
25
|
+
spec.add_development_dependency "sqlite3"
|
24
26
|
end
|
data/lib/httpsql.rb
CHANGED
@@ -1,42 +1,47 @@
|
|
1
1
|
require "httpsql/version"
|
2
2
|
|
3
3
|
module Httpsql
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def valid_params(params)
|
8
|
-
params.select{|k,v| column_names.include?(k.to_s.split('.').first)}
|
9
|
-
end
|
10
|
-
|
11
|
-
def where_params_eq(params={})
|
12
|
-
|
13
|
-
cond = valid_params(params).map do |k,v|
|
14
|
-
next if k.to_s == 'access_token'
|
15
|
-
(k, m) = k.to_s.split('.')
|
16
|
-
if m
|
17
|
-
arel_table[k.to_sym].send(m.to_sym, v)
|
18
|
-
elsif v.respond_to?(:any?)
|
19
|
-
arel_table[k.to_sym].in(v)
|
20
|
-
else
|
21
|
-
arel_table[k.to_sym].eq(v)
|
22
|
-
end
|
23
|
-
end.inject{|x,y| x.and(y)}
|
24
|
-
|
25
|
-
where(cond)
|
26
|
-
end
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
27
7
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
8
|
+
module ClassMethods
|
9
|
+
def where_params_eq(params={})
|
10
|
+
cond = valid_params(params).map do |k,v|
|
11
|
+
(k, m) = k.to_s.split('.')
|
12
|
+
next if k.to_s == 'access_token'
|
13
|
+
if m
|
14
|
+
arel_table[k.to_sym].send(m.to_sym, v)
|
15
|
+
elsif v.respond_to?(:any?)
|
16
|
+
arel_table[k.to_sym].in(v)
|
17
|
+
else
|
18
|
+
arel_table[k.to_sym].eq(v)
|
36
19
|
end
|
37
|
-
end
|
20
|
+
end.inject{|x,y| x.and(y)}
|
38
21
|
|
22
|
+
ar_rel = where(cond)
|
23
|
+
ar_rel = ar_rel.select(params[:field]) if params[:field]
|
24
|
+
ar_rel
|
25
|
+
end
|
26
|
+
|
27
|
+
def route_params
|
28
|
+
columns.inject({}) do |m,c|
|
29
|
+
m[c.name] = {
|
30
|
+
type: c.sql_type,
|
31
|
+
desc: c.name,
|
32
|
+
primary: c.primary
|
33
|
+
}
|
34
|
+
m
|
35
|
+
end.merge "field" => {
|
36
|
+
type: 'array',
|
37
|
+
desc: 'select fields',
|
38
|
+
primary: false
|
39
|
+
}
|
40
|
+
end
|
39
41
|
|
42
|
+
private
|
43
|
+
def valid_params(params)
|
44
|
+
params.select{|k,v| column_names.include?(k.to_s.split('.').first)}
|
40
45
|
end
|
41
46
|
end
|
42
47
|
end
|
data/lib/httpsql/version.rb
CHANGED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'active_record'
|
4
|
+
require 'httpsql'
|
5
|
+
|
6
|
+
ActiveRecord::Base.configurations[:test] = {adapter: 'sqlite3', database: 'tmp/httpsql_test'}
|
7
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[:test])
|
8
|
+
ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS foo_models }
|
9
|
+
ActiveRecord::Base.connection.execute %Q{
|
10
|
+
CREATE TABLE foo_models (
|
11
|
+
id integer,
|
12
|
+
int_field integer,
|
13
|
+
string_field text,
|
14
|
+
access_token text,
|
15
|
+
created_at text default CURRENT_TIMESTAMP,
|
16
|
+
updated_at text default CURRENT_TIMESTAMP,
|
17
|
+
primary key(id)
|
18
|
+
);
|
19
|
+
}
|
20
|
+
class FooModel < ActiveRecord::Base
|
21
|
+
include Httpsql
|
22
|
+
attr_accessible :int_field, :string_field, :access_token
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_models
|
26
|
+
FooModel.create!([
|
27
|
+
{int_field: 0, string_field: "zero", access_token: "000"},
|
28
|
+
{int_field: 1, string_field: "one", access_token: "111"},
|
29
|
+
{int_field: 2, string_field: "two", access_token: "222"},
|
30
|
+
{int_field: 3, string_field: "three", access_token: "333"},
|
31
|
+
{int_field: 4, string_field: "four", access_token: "444"},
|
32
|
+
])
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Httpsql do
|
36
|
+
before :each do
|
37
|
+
FooModel.connection.execute %Q{DELETE FROM foo_models}
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'selects a model\'s columns from a given hash' do
|
41
|
+
ret = FooModel.send(:valid_params, id: 1, int_field: 2, string_field: "foo", access_token: "a", created_at: '2013-01-01T00:00:00', created_at: '2013-01-01T00:00:00', foo: :bar)
|
42
|
+
ret.must_equal(id: 1, int_field: 2, string_field: "foo", access_token: "a", created_at: '2013-01-01T00:00:00', created_at: '2013-01-01T00:00:00')
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'selects all models' do
|
46
|
+
models = generate_models
|
47
|
+
FooModel.where_params_eq({}).must_equal models
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'selects a specified array of models' do
|
51
|
+
models = generate_models
|
52
|
+
FooModel.where_params_eq("int_field" => [0, 1]).must_equal models[0..1]
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'selects a model, using eq' do
|
56
|
+
models = generate_models
|
57
|
+
FooModel.where_params_eq("int_field.eq" => 0).must_equal [models[0]]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'selects models, using not_eq' do
|
61
|
+
models = generate_models
|
62
|
+
FooModel.where_params_eq("int_field.not_eq" => 0).must_equal models[1..-1]
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'selects a model, using matches' do
|
66
|
+
models = generate_models
|
67
|
+
FooModel.where_params_eq("string_field.matches" => "%hre%").must_equal [models[3]]
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'selects models, using does_not_match' do
|
71
|
+
models = generate_models
|
72
|
+
FooModel.where_params_eq("string_field.does_not_match" => "%ero").must_equal models[1..-1]
|
73
|
+
end
|
74
|
+
it 'selects models, using gt' do
|
75
|
+
models = generate_models
|
76
|
+
FooModel.where_params_eq("int_field.gt" => 1).must_equal models[2..-1]
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'selects models, using gteq' do
|
80
|
+
models = generate_models
|
81
|
+
FooModel.where_params_eq("int_field.gteq" => 2).must_equal models[2..-1]
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'select models, using lt' do
|
85
|
+
models = generate_models
|
86
|
+
FooModel.where_params_eq("int_field.lt" => 1).must_equal [models[0]]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'selects models, using lteq' do
|
90
|
+
models = generate_models
|
91
|
+
FooModel.where_params_eq("int_field.lteq" => 2).must_equal models[0..2]
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'selects models, using two ARel methods' do
|
95
|
+
models = generate_models
|
96
|
+
FooModel.where_params_eq("int_field.gteq" => 1, "id.gt" => 4).must_equal [models[4]]
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'ignores access_token' do
|
100
|
+
models = generate_models
|
101
|
+
FooModel.where_params_eq("access_token" => "111").must_equal models
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'ignores access_token dot notation' do
|
105
|
+
models = generate_models
|
106
|
+
FooModel.where_params_eq("access_token.eq" => "111").must_equal models
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'selects a model with specified fields' do
|
110
|
+
generate_models
|
111
|
+
model = FooModel.select([:int_field, :id]).where(int_field: 0)
|
112
|
+
FooModel.where_params_eq("int_field.eq" => 0, field: [:int_field, :id]).must_equal model
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'generates the correct documentation' do
|
116
|
+
FooModel.route_params.must_equal({
|
117
|
+
"id" => {:type => "integer", :desc => "id", :primary => true},
|
118
|
+
"int_field" => {:type => "integer", :desc => "int_field", :primary => false},
|
119
|
+
"string_field" => {:type => "text", :desc => "string_field", :primary => false},
|
120
|
+
"access_token" => {:type => "text", :desc => "access_token", :primary => false},
|
121
|
+
"created_at" => {:type => "text", :desc => "created_at", :primary => false},
|
122
|
+
"updated_at" => {:type => "text", :desc => "updated_at", :primary => false},
|
123
|
+
"field" => {:type => "array", :desc => "select fields", :primary => false}}
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
metadata
CHANGED
@@ -1,19 +1,22 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: httpsql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
|
-
-
|
8
|
+
- Philip Champon
|
8
9
|
- Alejandro Ciniglio
|
10
|
+
- Sean Shillo
|
9
11
|
autorequire:
|
10
12
|
bindir: bin
|
11
13
|
cert_chain: []
|
12
|
-
date: 2013-
|
14
|
+
date: 2013-07-03 00:00:00.000000000 Z
|
13
15
|
dependencies:
|
14
16
|
- !ruby/object:Gem::Dependency
|
15
17
|
name: bundler
|
16
18
|
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
17
20
|
requirements:
|
18
21
|
- - ~>
|
19
22
|
- !ruby/object:Gem::Version
|
@@ -21,6 +24,7 @@ dependencies:
|
|
21
24
|
type: :development
|
22
25
|
prerelease: false
|
23
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
24
28
|
requirements:
|
25
29
|
- - ~>
|
26
30
|
- !ruby/object:Gem::Version
|
@@ -28,35 +32,73 @@ dependencies:
|
|
28
32
|
- !ruby/object:Gem::Dependency
|
29
33
|
name: rake
|
30
34
|
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
31
36
|
requirements:
|
32
|
-
- - '>='
|
37
|
+
- - ! '>='
|
33
38
|
- !ruby/object:Gem::Version
|
34
39
|
version: '0'
|
35
40
|
type: :development
|
36
41
|
prerelease: false
|
37
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
38
44
|
requirements:
|
39
|
-
- - '>='
|
45
|
+
- - ! '>='
|
40
46
|
- !ruby/object:Gem::Version
|
41
47
|
version: '0'
|
42
48
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
49
|
+
name: activerecord
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.0'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '3.0'
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: grape
|
44
66
|
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
45
68
|
requirements:
|
46
|
-
- - '>='
|
69
|
+
- - ! '>='
|
47
70
|
- !ruby/object:Gem::Version
|
48
71
|
version: '0'
|
49
72
|
type: :runtime
|
50
73
|
prerelease: false
|
51
74
|
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: sqlite3
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
52
92
|
requirements:
|
53
|
-
- - '>='
|
93
|
+
- - ! '>='
|
54
94
|
- !ruby/object:Gem::Version
|
55
95
|
version: '0'
|
56
|
-
description:
|
96
|
+
description: Expose model columns and ARel methods through query parameters in grape
|
97
|
+
end points
|
57
98
|
email:
|
58
|
-
-
|
99
|
+
- philip@adaptly.com
|
59
100
|
- alejandro@adaptly.com
|
101
|
+
- sean@adaptly.com
|
60
102
|
executables: []
|
61
103
|
extensions: []
|
62
104
|
extra_rdoc_files: []
|
@@ -69,28 +111,32 @@ files:
|
|
69
111
|
- httpsql.gemspec
|
70
112
|
- lib/httpsql.rb
|
71
113
|
- lib/httpsql/version.rb
|
72
|
-
|
114
|
+
- test/httpsql_test.rb
|
115
|
+
homepage: https://github.com/Adaptly/httpsql
|
73
116
|
licenses:
|
74
117
|
- MIT
|
75
|
-
metadata: {}
|
76
118
|
post_install_message:
|
77
119
|
rdoc_options: []
|
78
120
|
require_paths:
|
79
121
|
- lib
|
80
122
|
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
81
124
|
requirements:
|
82
|
-
- - '>='
|
125
|
+
- - ! '>='
|
83
126
|
- !ruby/object:Gem::Version
|
84
127
|
version: '0'
|
85
128
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
86
130
|
requirements:
|
87
|
-
- - '>='
|
131
|
+
- - ! '>='
|
88
132
|
- !ruby/object:Gem::Version
|
89
133
|
version: '0'
|
90
134
|
requirements: []
|
91
135
|
rubyforge_project:
|
92
|
-
rubygems_version:
|
136
|
+
rubygems_version: 1.8.23
|
93
137
|
signing_key:
|
94
|
-
specification_version:
|
95
|
-
summary:
|
96
|
-
|
138
|
+
specification_version: 3
|
139
|
+
summary: Select model specified fields, create arbitrary queries, all using CGI query
|
140
|
+
parameters
|
141
|
+
test_files:
|
142
|
+
- test/httpsql_test.rb
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 9fd889994ae94496fde8767ceb1aeedb6bf7b580
|
4
|
-
data.tar.gz: 9d54f043cfac281572b16ba80d02379d142824a4
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: 47fd7364530a7db44688280d83c3c8f23a5f72379b9b2948abf3beb1a9f6f87b21bff82cd5fc7757362886db53ce79150ddef029b360a1268d7a8358a6c7590c
|
7
|
-
data.tar.gz: b7ca268a828be986d6b1186c411af54af0e4bd4b1f997a563c0abb487b827168ef1d9387d8ddc2d9072d9be4b311eb2096951887c6233ee36d6eadd2f3aba8f7
|