httpsql 0.1.2 → 0.2.0

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.
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ repo_token: mUabjcN9fjNpphNtEUHYDJtE1VzEa8p12
data/.gitignore CHANGED
@@ -15,3 +15,6 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *.sw[a-z]
19
+ *~
20
+ *.orig
data/README.md CHANGED
@@ -1,26 +1,31 @@
1
1
  # Httpsql
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/httpsql.png)](http://badge.fury.io/rb/httpsql)
4
- [![Build Status](https://travis-ci.org/Adaptly/httpsql.png)](https://travis-ci.org/Adaptly/httpsql)
4
+ [![Build Status](https://travis-ci.org/Adaptly/httpsql.png?branch=master)](https://travis-ci.org/Adaptly/httpsql)
5
5
  [![Code Climate](https://codeclimate.com/github/Adaptly/httpsql.png)](https://codeclimate.com/github/Adaptly/httpsql)
6
6
  [![Dependency Status](https://gemnasium.com/Adaptly/httpsql.png)](https://gemnasium.com/Adaptly/httpsql)
7
7
  [![Coverage Status](https://coveralls.io/repos/Adaptly/httpsql/badge.png)](https://coveralls.io/r/Adaptly/httpsql)
8
8
 
9
- Httpsql is a module, designed to be included in [Active Record](http://api.rubyonrails.org/classes/ActiveRecord/Base.html)
10
- models exposed by [grape](https://github.com/intridea/grape). Once the module is
11
- included, a given model can respond directly to query params passed to it, using
12
- `where_params_eq`. You can also constrain the fields returned by the model,
13
- using the `fields` query parameter.
9
+ Httpsql is a module, designed to be included in [Active
10
+ Record](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) models
11
+ exposed by [grape](https://github.com/intridea/grape). Once the module is
12
+ included, a given model can respond directly to query params passed to it,
13
+ using `with_params`. You can constrain the fields returned by the model, using
14
+ the `field` query parameter. You can also use httpsql to group, join, and order
15
+ results. Currently, only inner joins are supported, using rudimentary Active
16
+ Record joins.
14
17
 
15
18
  Httpsql uses [ARel](http://www.slideshare.net/flah00/activerecord-arel) to
16
19
  generate queries and exposes ARel's methods via query params. The supported ARel
17
- methods are eq, not_eq, matches, does_not_match, gt, gteq, lt, lteq.
20
+ methods are eq, not_eq, matches, does_not_match, gt, gteq, lt, lteq, sum,
21
+ maximum, and minimum. It is also possible to pass columns to arbitrary SQL
22
+ functions.
18
23
 
19
24
  Httpsql also generates documentaion for endpoints, which can be easily merged
20
- into your existing documentation (`#route_params`).
25
+ into your existing documentation (`#grape_documentation`).
21
26
 
22
- Httpsql reserves one parameter, access_token. If your model has a field called
23
- access_token, you'll need to rename it.
27
+ Httpsql reserves the following parameters access_token, field, group, join, and order. If
28
+ your model has any columns by the same name, you'll need to rename them.
24
29
 
25
30
  ## Installation
26
31
 
@@ -48,13 +53,29 @@ Assume you have a model, widget, whose fields are id, int_field, string_field, c
48
53
  t.datetime "updated_at", :null => false
49
54
  end
50
55
 
51
- ### model.rb
56
+ create_table "dongles", :force => true do |t|
57
+ t.integer "widget_id"
58
+ t.string "description"
59
+ t.datetime "created_at", :null => false
60
+ t.datetime "updated_at", :null => false
61
+ end
62
+
63
+ ### widget.rb
52
64
 
53
65
  class Widget < ActiveRecord::Base
54
66
  include Httpsql
67
+ has_many :dongles
55
68
  attr_accessible :int_field, :dec_field, :string_field
56
69
  end
57
70
 
71
+ ### dongle.rb
72
+
73
+ class Dongle < ActiveRecord::Base
74
+ include Httpsql
75
+ belongs_to :widget
76
+ attr_accessible :description
77
+ end
78
+
58
79
  ### api.rb
59
80
 
60
81
  class Api < Grape::API
@@ -63,13 +84,25 @@ Assume you have a model, widget, whose fields are id, int_field, string_field, c
63
84
  default_format :json
64
85
 
65
86
  resource :widgets do
66
- desc 'Get all widgets', {
67
- optional_params: Widget.route_params
68
- }
87
+ desc 'Get all widgets'
88
+ params do
89
+ Widget.grape_documentation(self)
90
+ end
69
91
  get '/' do
70
- present Widget.where_params_eq(params)
92
+ Widget.with_params(params)
71
93
  end
72
94
  end
95
+
96
+ resource :dongles do
97
+ desc 'Get all dongles'
98
+ params do
99
+ Dongle.grape_documentation(self)
100
+ end
101
+ get '/' do
102
+ Dongle.with_params(params)
103
+ end
104
+ end
105
+
73
106
  end
74
107
 
75
108
  ### config.ru
@@ -102,9 +135,18 @@ Query your new API
102
135
  curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01'
103
136
  SELECT * FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
104
137
 
105
- curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01&fields[]=id&fields[]=int_field'
138
+ curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01&field[]=id&field[]=int_field'
106
139
  SELECT id, int_field FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
107
140
 
141
+ curl 'http://localhost:3000/api/v1/widgets?order=widgets.int_field+desc'
142
+ SELECT * FROM widgets ORDER BY widgets.int_field desc
143
+
144
+ curl 'http://localhost:3000/api/v1/widgets?group=widgets.int_field'
145
+ SELECT * FROM widgets GROUP BY widgets.int_field
146
+
147
+ curl 'http://localhost:3000/api/v1/dongles?field[]=description&field[]=widgets.int_field&join[]=widgets&group[]=widgets.string_field&order=string_field'
148
+ SELECT dongles.description, widgets.int_field FROM dongles INNER JOIN widgets ON (widgets.id = dongles.widget_id) GROUP BY widgets.string_field ORDER BY dongles.string_field
149
+
108
150
  curl 'http://localhost:3000/api/v1/widgets?int_field.sum'
109
151
  SELECT SUM(int_field) AS int_field FROM widgets
110
152
 
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
  gem "activerecord", "~> 3.1.0"
3
- gem "grape"
3
+ gem "grape", ">= 0.5.0"
4
4
  gem "bundler", "~> 1.3"
5
5
  gem "coveralls", ">= 0.5.7"
6
6
  gem "rake"
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
  gem "activerecord", "~> 3.2.0"
3
- gem "grape"
3
+ gem "grape", ">= 0.5.0"
4
4
  gem "bundler", "~> 1.3"
5
5
  gem "coveralls", ">= 0.5.7"
6
6
  gem "rake"
@@ -1,6 +1,7 @@
1
1
  source "https://rubygems.org"
2
+ gem "minitest", "~> 4.2"
2
3
  gem "activerecord", "~> 4.0.0"
3
- gem "grape"
4
+ gem "grape", ">= 0.5.0"
4
5
  gem "bundler", "~> 1.3"
5
6
  gem "coveralls", ">= 0.5.7"
6
7
  gem "rake"
data/httpsql.gemspec CHANGED
@@ -18,11 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", ">= 3.1"
21
+ spec.add_dependency "activerecord", "~> 3.2"
22
22
  spec.add_dependency "arel", ">= 2.2"
23
- spec.add_dependency "grape"
23
+ spec.add_dependency "grape", ">= 0.5.0"
24
24
  spec.add_development_dependency "bundler", "~> 1.3"
25
25
  spec.add_development_dependency "coveralls", ">= 0.5.7"
26
+ spec.add_development_dependency "minitest", "~> 4.2"
27
+ spec.add_development_dependency "pry-nav"
26
28
  spec.add_development_dependency "rake"
27
29
  spec.add_development_dependency "simplecov", ">= 0.7"
28
30
  spec.add_development_dependency "sqlite3"
data/lib/httpsql.rb CHANGED
@@ -6,14 +6,35 @@ module Httpsql
6
6
  end
7
7
 
8
8
  module ClassMethods
9
- def where_params_eq(params={})
10
- fields = params[:field] || []
11
- conds = []
9
+ # The method to call within API end points
10
+ # @param params [Hash] The params hash for a given API request
11
+ # @return [ActiveRecord::Relation]
12
+ # @example Constraining a model index end point
13
+ # resource :my_models
14
+ # get '/' do
15
+ # MyModel.with_params(params)
16
+ # end
17
+ # end
18
+ def with_params(params={})
19
+ fields = []
20
+ conds = []
21
+ joins = []
22
+ groups = []
23
+ orders = []
12
24
 
13
- valid_params(params).each do |k,v|
14
- (k, m) = k.to_s.split('.')
25
+ fields += Array(params[:field] || params['field'])
26
+
27
+ httpsql_valid_params(params).each do |k,v|
28
+ k, m = k.to_s.split('.')
15
29
  next if k.to_s == 'access_token'
16
- if m
30
+
31
+ if k == 'join'
32
+ joins += Array(v).map!(&:to_sym)
33
+ elsif k == 'order'
34
+ orders += Array(v).map!{|w| httpsql_quote_value_with_args(w)}
35
+ elsif k == 'group'
36
+ groups += Array(v).map!{|w| httpsql_quote_value(w)}
37
+ elsif m
17
38
  if %w(sum minimum maximum).include?(m)
18
39
  fields << arel_table[k].send(m).as(k)
19
40
  elsif !arel_table[k].respond_to?(m)
@@ -32,29 +53,71 @@ module Httpsql
32
53
  conds = conds.compact.inject{|x,y| x.and(y)}
33
54
 
34
55
  ar_rel = where(conds)
56
+ ar_rel = ar_rel.joins(joins) if joins.any?
57
+ ar_rel = ar_rel.group(groups) if groups.any?
58
+ ar_rel = ar_rel.order(orders) if orders.any?
35
59
  ar_rel = ar_rel.select(fields) if fields.any?
36
60
  ar_rel
37
61
  end
38
62
 
39
- def route_params
40
- columns.inject({}) do |m,c|
41
- m[c.name] = {
42
- type: c.sql_type,
43
- desc: c.name,
44
- primary: c.primary
45
- }
46
- m
47
- end.merge "field" => {
48
- type: 'array',
49
- desc: 'select fields',
50
- primary: false
51
- }
63
+ # Provide documentation for Grape end points
64
+ # @param ctx [Object] The calling object
65
+ # @return [Hash]
66
+ # @example Including documentation
67
+ # params do
68
+ # MyModel.grape_documentation(self)
69
+ # end
70
+ def grape_documentation(ctx=nil)
71
+ columns.each do |c|
72
+ opt_hash = {}
73
+ if (k = httpsql_sql_type_conversion(c.type))
74
+ opt_hash[:type] = k
75
+ end
76
+ ctx.optional c.name, opt_hash
77
+ end
78
+ ctx.optional :field, type: Array, desc: "An array of strings: fields to select from the database"
79
+ ctx.optional :group, type: Array, desc: "An array of strings: fields to group by"
80
+ ctx.optional :order, type: Array, desc: "An array of strings: fields to order by"
81
+ ctx.optional :join, type: Array, desc: "An array of strings: tables to join (#{httpsql_join_tables.join(',')})"
52
82
  end
53
83
 
54
84
  private
55
- def valid_params(params)
56
- params.select{|k,v| column_names.include?(k.to_s.split('.').first)}
85
+ def httpsql_valid_params(params)
86
+ params.select{|k,v| [*column_names, 'join', 'group', 'order'].include?(k.to_s.split('.').first)}
57
87
  end
88
+
89
+ def httpsql_quote_value(v)
90
+ v['.'].nil? ? arel_table[v] : v.split('.').map!{|x| connection.quote_table_name(x)}.join('.')
91
+ end
92
+
93
+ def httpsql_quote_value_with_args(v)
94
+ match = v.match(/([^\s]+)(?:\s+(.*))?/)
95
+ q = httpsql_quote_value(match[1])
96
+ if match[2]
97
+ if q.kind_of?(String)
98
+ q + " " + match[2]
99
+ else
100
+ q.send(match[2])
101
+ end
102
+ end
103
+ end
104
+
105
+ def httpsql_sql_type_conversion(type)
106
+ case type.to_sym
107
+ when :bigint then Bignum
108
+ when :date then Date
109
+ when :datetime then Time
110
+ when :float, :decimal then Float
111
+ when :integer then Fixnum
112
+ when :string, /text/, /^char/, /^varchar/ then String
113
+ else nil
114
+ end
115
+ end
116
+
117
+ def httpsql_join_tables
118
+ @join_tables ||= reflections.keys
119
+ end
120
+
58
121
  end
59
122
  end
60
123
 
@@ -1,3 +1,3 @@
1
1
  module Httpsql
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/httpsql_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- def generate_models
3
+ def generate_foo_models
4
4
  FooModel.create!([
5
5
  {int_field: 0, dec_field: 0.01, string_field: "zero", access_token: "000"},
6
6
  {int_field: 1, dec_field: 1.01, string_field: "one", access_token: "111"},
@@ -10,120 +10,269 @@ def generate_models
10
10
  ])
11
11
  end
12
12
 
13
+ def generate_bar_models
14
+ BarModel.create!([
15
+ {foo_model_id: 1, string_field: "zero"},
16
+ {foo_model_id: 2, string_field: "one"},
17
+ ])
18
+ end
19
+
20
+ def generate_baz_models
21
+ BazModel.create!([
22
+ {foo_model_id: 1, string_field: "zeropointzero"},
23
+ {foo_model_id: 1, string_field: "zeropointone"},
24
+ {foo_model_id: 2, string_field: "onepointzero"},
25
+ {foo_model_id: 2, string_field: "onepointone"},
26
+ ])
27
+ end
28
+
13
29
  describe Httpsql do
14
30
  before :each do
15
31
  FooModel.connection.execute %Q{DELETE FROM foo_models}
32
+ FooModel.connection.execute %Q{DELETE FROM bar_models}
33
+ FooModel.connection.execute %Q{DELETE FROM baz_models}
16
34
  end
17
35
 
18
- it 'selects a model\'s columns from a given hash' do
19
- 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)
20
- 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')
21
- end
36
+ describe "#httpsql_valid_params" do
37
+ it 'selects a model\'s columns from a given hash' do
38
+ ret = FooModel.send(:httpsql_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)
39
+ 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')
40
+ end
22
41
 
23
- it 'selects all models' do
24
- models = generate_models
25
- FooModel.where_params_eq({}).must_equal models
42
+ it 'selects reserved parameters' do
43
+ ret = FooModel.send(:httpsql_valid_params, 'join'=>'foo', 'group'=>'bar', 'order'=>'baz')
44
+ ret.must_equal('join'=>'foo', 'group'=>'bar', 'order'=>'baz')
45
+ end
26
46
  end
27
47
 
28
- it 'selects a specified array of models' do
29
- models = generate_models
30
- FooModel.where_params_eq("int_field" => [0, 1]).must_equal models[0..1]
31
- end
48
+ describe '#with_params' do
49
+ it 'selects all models' do
50
+ models = generate_foo_models
51
+ FooModel.with_params({}).must_equal models
52
+ end
32
53
 
33
- it 'selects a model, using eq' do
34
- models = generate_models
35
- FooModel.where_params_eq("int_field.eq" => 0).must_equal [models[0]]
36
- end
54
+ it 'selects a specified array of models' do
55
+ models = generate_foo_models
56
+ FooModel.with_params("int_field" => [0, 1]).must_equal models[0..1]
57
+ end
37
58
 
38
- it 'selects models, using not_eq' do
39
- models = generate_models
40
- FooModel.where_params_eq("int_field.not_eq" => 0).must_equal models[1..-1]
41
- end
59
+ it 'selects a model, using eq' do
60
+ models = generate_foo_models
61
+ FooModel.with_params("int_field.eq" => 0).must_equal [models[0]]
62
+ end
42
63
 
43
- it 'selects a model, using matches' do
44
- models = generate_models
45
- FooModel.where_params_eq("string_field.matches" => "%hre%").must_equal [models[3]]
46
- end
64
+ it 'selects models, using not_eq' do
65
+ models = generate_foo_models
66
+ FooModel.with_params("int_field.not_eq" => 0).must_equal models[1..-1]
67
+ end
47
68
 
48
- it 'selects models, using does_not_match' do
49
- models = generate_models
50
- FooModel.where_params_eq("string_field.does_not_match" => "%ero").must_equal models[1..-1]
51
- end
52
- it 'selects models, using gt' do
53
- models = generate_models
54
- FooModel.where_params_eq("int_field.gt" => 1).must_equal models[2..-1]
55
- end
69
+ it 'selects a model, using matches' do
70
+ models = generate_foo_models
71
+ FooModel.with_params("string_field.matches" => "%hre%").must_equal [models[3]]
72
+ end
56
73
 
57
- it 'selects models, using gteq' do
58
- models = generate_models
59
- FooModel.where_params_eq("int_field.gteq" => 2).must_equal models[2..-1]
60
- end
74
+ it 'selects models, using does_not_match' do
75
+ models = generate_foo_models
76
+ FooModel.with_params("string_field.does_not_match" => "%ero").must_equal models[1..-1]
77
+ end
78
+ it 'selects models, using gt' do
79
+ models = generate_foo_models
80
+ FooModel.with_params("int_field.gt" => 1).must_equal models[2..-1]
81
+ end
61
82
 
62
- it 'select models, using lt' do
63
- models = generate_models
64
- FooModel.where_params_eq("int_field.lt" => 1).must_equal [models[0]]
65
- end
83
+ it 'selects models, using gteq' do
84
+ models = generate_foo_models
85
+ FooModel.with_params("int_field.gteq" => 2).must_equal models[2..-1]
86
+ end
66
87
 
67
- it 'selects models, using lteq' do
68
- models = generate_models
69
- FooModel.where_params_eq("int_field.lteq" => 2).must_equal models[0..2]
70
- end
88
+ it 'select models, using lt' do
89
+ models = generate_foo_models
90
+ FooModel.with_params("int_field.lt" => 1).must_equal [models[0]]
91
+ end
71
92
 
72
- it 'selects models, using two ARel methods' do
73
- models = generate_models
74
- FooModel.where_params_eq("int_field.gteq" => 1, "id.gt" => 4).must_equal [models[4]]
75
- end
93
+ it 'selects models, using lteq' do
94
+ models = generate_foo_models
95
+ FooModel.with_params("int_field.lteq" => 2).must_equal models[0..2]
96
+ end
76
97
 
77
- it 'ignores access_token' do
78
- models = generate_models
79
- FooModel.where_params_eq("access_token" => "111").must_equal models
80
- end
98
+ it 'selects models, using two ARel methods' do
99
+ models = generate_foo_models
100
+ FooModel.with_params("int_field.gteq" => 1, "id.gt" => 4).must_equal [models[4]]
101
+ end
81
102
 
82
- it 'ignores access_token dot notation' do
83
- models = generate_models
84
- FooModel.where_params_eq("access_token.eq" => "111").must_equal models
85
- end
103
+ it 'ignores access_token' do
104
+ models = generate_foo_models
105
+ FooModel.with_params("access_token" => "111").must_equal models
106
+ end
86
107
 
87
- it 'selects a model with specified fields' do
88
- generate_models
89
- model = FooModel.select([:int_field, :id]).where(int_field: 0)
90
- FooModel.where_params_eq("int_field.eq" => 0, field: [:int_field, :id]).must_equal model
91
- end
108
+ it 'ignores access_token dot notation' do
109
+ models = generate_foo_models
110
+ FooModel.with_params("access_token.eq" => "111").must_equal models
111
+ end
112
+
113
+ it 'selects a model with specified fields' do
114
+ generate_foo_models
115
+ model = FooModel.select([:int_field, :id]).where(int_field: 0)
116
+ FooModel.with_params("int_field.eq" => 0, field: [:int_field, :id]).must_equal model
117
+ end
118
+
119
+ it 'sums the specified field' do
120
+ models = generate_foo_models
121
+ expected = models.collect(&:int_field).inject(:+)
122
+ FooModel.with_params("int_field.sum" => nil).first.int_field.must_equal expected
123
+ end
124
+
125
+ it 'selects the maximum value for the specified field' do
126
+ models = generate_foo_models
127
+ expected = models.collect(&:int_field).max
128
+ FooModel.with_params("int_field.maximum" => nil).first.int_field.must_equal expected
129
+ end
130
+
131
+ it 'selects the minimum value for the specified field' do
132
+ models = generate_foo_models
133
+ expected = models.collect(&:int_field).min
134
+ FooModel.with_params("int_field.minimum" => nil).first.int_field.must_equal expected
135
+ end
136
+
137
+ it 'generates the specified sql for rounding' do
138
+ models = generate_foo_models
139
+ expected = models.collect(&:dec_field).map{|v| v.round.to_f}
140
+ FooModel.with_params("dec_field.round" => "1").collect(&:dec_field).map{|v| v.to_f}.must_equal expected
141
+ end
142
+
143
+ it 'groups correctly' do
144
+ models = generate_foo_models
145
+ expected = [FooModel.create({created_at: "1900-01-01"}), models.last]
146
+ FooModel.with_params("group" => "created_at").must_equal expected
147
+ end
148
+
149
+ it 'orders unqualified fields correctly' do
150
+ models = generate_foo_models
151
+ expected = models.reverse
152
+ FooModel.with_params("order" => "int_field desc").must_equal expected
153
+ end
154
+
155
+ it 'orders qualified fields correctly' do
156
+ models = generate_foo_models
157
+ expected = models.reverse
158
+ FooModel.with_params("order" => "foo_models.int_field desc").must_equal expected
159
+ end
160
+
161
+ it 'joins has_one relations' do
162
+ models = generate_foo_models
163
+ generate_bar_models
164
+ FooModel.with_params("join" => "bar_model").to_a.must_equal models[0..1]
165
+ end
166
+
167
+ it 'joins has_many relations' do
168
+ models = generate_foo_models
169
+ generate_baz_models
170
+ FooModel.with_params("join" => "baz_models").to_a.must_equal [models[0], models[0], models[1], models[1]]
171
+ end
172
+
173
+ it 'joins has_many relations and uses field and group' do
174
+ models = generate_foo_models
175
+ generate_baz_models
176
+ expected = [
177
+ {"int_field"=>1, "string_field"=>"onepointone"},
178
+ {"int_field"=>1, "string_field"=>"onepointzero"},
179
+ {"int_field"=>0, "string_field"=>"zeropointone"},
180
+ {"int_field"=>0, "string_field"=>"zeropointzero"},
181
+ ]
182
+ ## TODO: sqlite3 && activerecord-4.0 insert an id field... why!?
183
+ expected.map! do |e|
184
+ e["id"] = nil
185
+ e
186
+ end if ActiveRecord::VERSION::MAJOR >= 4
187
+ query = BazModel.with_params("join" => "foo_model",
188
+ "field" => ["foo_models.int_field", "baz_models.string_field"],
189
+ "group" => ["baz_models.string_field"],
190
+ "order" => ["baz_models.string_field"])
191
+ query.collect(&:attributes).must_equal expected
192
+ end
92
193
 
93
- it 'sums the specified field' do
94
- models = generate_models
95
- expected = models.collect(&:int_field).inject(:+)
96
- FooModel.where_params_eq("int_field.sum" => nil).first.int_field.must_equal expected
97
194
  end
98
195
 
99
- it 'selects the maximum value for the specified field' do
100
- models = generate_models
101
- expected = models.collect(&:int_field).max
102
- FooModel.where_params_eq("int_field.maximum" => nil).first.int_field.must_equal expected
196
+ describe "#httpsql_quote_value" do
197
+ it "quotes absolute names" do
198
+ FooModel.send(:httpsql_quote_value, "foo.bar").must_equal "\"foo\".\"bar\""
199
+ end
200
+
201
+ it "converts column names to arel nodes" do
202
+ FooModel.send(:httpsql_quote_value, "int_field").must_be_kind_of Arel::Attributes::Attribute
203
+ end
103
204
  end
104
205
 
105
- it 'selects the minimum value for the specified field' do
106
- models = generate_models
107
- expected = models.collect(&:int_field).min
108
- FooModel.where_params_eq("int_field.minimum" => nil).first.int_field.must_equal expected
206
+ describe "#httpsql_quote_value_with_args" do
207
+ it "quotes absolute names" do
208
+ FooModel.send(:httpsql_quote_value, "foo.bar").must_equal "\"foo\".\"bar\""
209
+ end
210
+
211
+ it "quotes absolute names with args" do
212
+ FooModel.send(:httpsql_quote_value_with_args, "foo.bar baz").must_equal "\"foo\".\"bar\" baz"
213
+ end
109
214
  end
110
215
 
111
- it 'generates the specified sql for rounding' do
112
- models = generate_models
113
- expected = models.collect(&:dec_field).map{|v| v.round.to_f}
114
- FooModel.where_params_eq("dec_field.round" => "1").collect(&:dec_field).map{|v| v.to_f}.must_equal expected
216
+ describe '#httpsql_sql_type_conversion' do
217
+ it 'identifies bigint as Bignum' do
218
+ FooModel.send(:httpsql_sql_type_conversion, 'bigint').must_equal Bignum
219
+ end
220
+
221
+ it 'identifies date as Date' do
222
+ FooModel.send(:httpsql_sql_type_conversion, 'date').must_equal Date
223
+ end
224
+
225
+ it 'identifies datetime as Time' do
226
+ FooModel.send(:httpsql_sql_type_conversion, 'datetime').must_equal Time
227
+ end
228
+
229
+ it 'identifies float as Float' do
230
+ FooModel.send(:httpsql_sql_type_conversion, 'float').must_equal Float
231
+ end
232
+
233
+ it 'identifies decimal as Bignum' do
234
+ FooModel.send(:httpsql_sql_type_conversion, 'decimal').must_equal Float
235
+ end
236
+
237
+ it 'identifies integer as Fixnum' do
238
+ FooModel.send(:httpsql_sql_type_conversion, 'integer').must_equal Fixnum
239
+ end
240
+
241
+ it 'identifies string as String' do
242
+ FooModel.send(:httpsql_sql_type_conversion, 'string').must_equal String
243
+ end
244
+
245
+ it 'identifies text as String' do
246
+ FooModel.send(:httpsql_sql_type_conversion, 'text').must_equal String
247
+ end
248
+
249
+ it 'identifies character varying(255) as String' do
250
+ FooModel.send(:httpsql_sql_type_conversion, 'character varying(255)').must_equal String
251
+ end
252
+
253
+ it 'identifies something as nil' do
254
+ FooModel.send(:httpsql_sql_type_conversion, 'something').must_be_nil
255
+ end
115
256
  end
116
257
 
117
- it 'generates the correct documentation' do
118
- FooModel.route_params.must_equal({
119
- "id" => {:type => "integer", :desc => "id", :primary => true},
120
- "int_field" => {:type => "integer", :desc => "int_field", :primary => false},
121
- "dec_field" => {:type => "decimal", :desc => "dec_field", :primary => false},
122
- "string_field" => {:type => "text", :desc => "string_field", :primary => false},
123
- "access_token" => {:type => "text", :desc => "access_token", :primary => false},
124
- "created_at" => {:type => "text", :desc => "created_at", :primary => false},
125
- "updated_at" => {:type => "text", :desc => "updated_at", :primary => false},
126
- "field" => {:type => "array", :desc => "select fields", :primary => false}}
127
- )
258
+ describe '#grape_documentation' do
259
+ it 'generates the correct documentation for version 0.5.x' do
260
+ TestApi.routes.first.route_params.must_equal({
261
+ "id" => {:required => false, :type => "Fixnum"},
262
+ "int_field" => {:required => false, :type => "Fixnum"},
263
+ "dec_field" => {:required => false, :type => "Float"},
264
+ "string_field" => {:required => false, :type => "String"},
265
+ "access_token" => {:required => false, :type => "String"},
266
+ "created_at" => {:required => false, :type => "String"},
267
+ "updated_at" => {:required => false, :type => "String"},
268
+ "field" => {:required => false, :type => "Array", :desc => "An array of strings: fields to select from the database"},
269
+ "group" => {:required => false, :type => "Array", :desc => "An array of strings: fields to group by"},
270
+ "order" => {:required => false, :type => "Array", :desc => "An array of strings: fields to order by"},
271
+ "join" => {:required => false, :type => "Array", :desc => "An array of strings: tables to join (bar_model,baz_models)"}
272
+
273
+ })
274
+ end
275
+
128
276
  end
277
+
129
278
  end
data/test/test_helper.rb CHANGED
@@ -1,12 +1,14 @@
1
+ require 'simplecov'
2
+ #SimpleCov.start
3
+ require 'coveralls'
4
+ Coveralls.wear!
5
+
1
6
  require 'minitest/spec'
2
7
  require 'minitest/autorun'
3
8
  require 'active_record'
9
+ require 'grape'
4
10
  require 'httpsql'
5
11
 
6
- require 'simplecov'
7
- require 'coveralls'
8
- Coveralls.wear!
9
-
10
12
  ActiveRecord::Base.configurations[:test] = {adapter: 'sqlite3', database: 'tmp/httpsql_test'}
11
13
  ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[:test])
12
14
  ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS foo_models }
@@ -22,8 +24,79 @@ ActiveRecord::Base.connection.execute %Q{
22
24
  primary key(id)
23
25
  );
24
26
  }
27
+
28
+ ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS bar_models }
29
+ ActiveRecord::Base.connection.execute %Q{
30
+ CREATE TABLE bar_models (
31
+ id integer,
32
+ foo_model_id integer,
33
+ string_field text,
34
+ primary key(id)
35
+ );
36
+ }
37
+
38
+ ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS baz_models }
39
+ ActiveRecord::Base.connection.execute %Q{
40
+ CREATE TABLE baz_models (
41
+ id integer,
42
+ foo_model_id integer,
43
+ string_field text,
44
+ primary key(id)
45
+ );
46
+ }
47
+
48
+ ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS bam_models }
49
+ ActiveRecord::Base.connection.execute %Q{
50
+ CREATE TABLE bam_models (
51
+ id integer,
52
+ bar_model_id integer,
53
+ string_field text,
54
+ primary key(id)
55
+ );
56
+ }
57
+
25
58
  class FooModel < ActiveRecord::Base
26
59
  include Httpsql
27
- #attr_accessible :int_field, :dec_field, :string_field, :access_token
60
+ has_one :bar_model
61
+ has_many :baz_models
62
+ end
63
+
64
+ class BarModel < ActiveRecord::Base
65
+ include Httpsql
66
+ belongs_to :foo_model
67
+ has_many :bam_models
68
+ end
69
+
70
+ class BazModel < ActiveRecord::Base
71
+ include Httpsql
72
+ belongs_to :foo_model
73
+ end
74
+
75
+ class BamModel < ActiveRecord::Base
76
+ include Httpsql
77
+ belongs_to :bar_model
78
+ end
79
+
80
+ class TestApi < Grape::API
81
+ resource :foo_models do
82
+ desc 'foo models index'
83
+ params do
84
+ FooModel.grape_documentation(self)
85
+ end
86
+ get '/' do
87
+ FooModel.with_params(params)
88
+ end
89
+ end
90
+
91
+ resource :baz_models do
92
+ desc 'baz models index'
93
+ params do
94
+ BazModel.grape_documentation(self)
95
+ end
96
+ get '/' do
97
+ BazModel.with_params(params)
98
+ end
99
+ end
100
+
28
101
  end
29
102
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpsql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,24 +11,24 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-07-18 00:00:00.000000000 Z
14
+ date: 2013-07-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activerecord
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
- - - ! '>='
21
+ - - ~>
22
22
  - !ruby/object:Gem::Version
23
- version: '3.1'
23
+ version: '3.2'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
27
27
  none: false
28
28
  requirements:
29
- - - ! '>='
29
+ - - ~>
30
30
  - !ruby/object:Gem::Version
31
- version: '3.1'
31
+ version: '3.2'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: arel
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -52,7 +52,7 @@ dependencies:
52
52
  requirements:
53
53
  - - ! '>='
54
54
  - !ruby/object:Gem::Version
55
- version: '0'
55
+ version: 0.5.0
56
56
  type: :runtime
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
@@ -60,7 +60,7 @@ dependencies:
60
60
  requirements:
61
61
  - - ! '>='
62
62
  - !ruby/object:Gem::Version
63
- version: '0'
63
+ version: 0.5.0
64
64
  - !ruby/object:Gem::Dependency
65
65
  name: bundler
66
66
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +93,38 @@ dependencies:
93
93
  - - ! '>='
94
94
  - !ruby/object:Gem::Version
95
95
  version: 0.5.7
96
+ - !ruby/object:Gem::Dependency
97
+ name: minitest
98
+ requirement: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '4.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ~>
110
+ - !ruby/object:Gem::Version
111
+ version: '4.2'
112
+ - !ruby/object:Gem::Dependency
113
+ name: pry-nav
114
+ requirement: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
96
128
  - !ruby/object:Gem::Dependency
97
129
  name: rake
98
130
  requirement: !ruby/object:Gem::Requirement
@@ -151,6 +183,7 @@ executables: []
151
183
  extensions: []
152
184
  extra_rdoc_files: []
153
185
  files:
186
+ - .coveralls.yml
154
187
  - .gitignore
155
188
  - .travis.yml
156
189
  - Gemfile