muster 0.0.6 → 0.0.7

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/Authors ADDED
@@ -0,0 +1,2 @@
1
+ - Chris Laco <claco@chrislaco.com?
2
+ - Nikica Jokić (neektza@gmail.com)
data/Changes CHANGED
@@ -1,3 +1,15 @@
1
+ = 0.0.7
2
+
3
+ * Added JoinsExpressions to ActiveRecord Strategy (neektza)
4
+
5
+ = 0.0.6
6
+
7
+ * Moved from OpenString to method_missing for dot notation on Results
8
+
9
+ = 0.0.5
10
+
11
+ * Added OpenStruct for dot notation on Results
12
+
1
13
  = 0.0.4
2
14
 
3
15
  * Rack middleware now returns Muster::Results
data/README.md CHANGED
@@ -52,6 +52,12 @@ Returns options with support for dirctional indicators for use in sorting.
52
52
  ?order=name&order=age #=> { 'order' => ['name asc', 'age asc'] }
53
53
  ?order=name:asc&age:desc #=> { 'order' => ['name asc', 'age desc'] }
54
54
 
55
+ ### JoinsExpression
56
+
57
+ Returns an ActiveRecord style array of 'joins' options.
58
+
59
+ ?joins=author.name,activity #=> { 'joins' => [{'author' => 'name'}, 'activity'] }
60
+
55
61
  ### Pagination
56
62
 
57
63
  Returns options to support pagination with logic for default page size, not-a-number checks and offset calculations.
@@ -64,12 +70,25 @@ Returns options to support pagination with logic for default page size, not-a-nu
64
70
 
65
71
  Combines many of the strategies above to output ActiveRecord Query interface compatible options.
66
72
 
67
- ?select=id,name&where=status:new&order=name:desc&page=3&page_size=10
73
+ ?select=id,name&where=status:new&order=name:desc&joins=author.name,activity&page=3&page_size=10
68
74
 
69
- { 'select' => ['id', 'name'], 'where' => {'status' => 'new'}, 'order' => 'name desc', 'limit' => 10, 'offset' => 20, 'pagination' => {:page => 3, :per_page => 10} }
75
+ {
76
+ 'select' => ['id', 'name'],
77
+ 'where' => {'status' => 'new'},
78
+ 'order' => 'name desc',
79
+ 'limit' => 10,
80
+ 'offset' => 20,
81
+ 'pagination' => {:page => 3, :per_page => 10},
82
+ 'joins' => [{'author' => 'name'}, 'activity']
83
+ }
70
84
 
71
85
  query = env['muster.query']
72
- Person.select( query[:select] ).where( query[:where] ).order( query[:order] ).offset( query[:offset] ).limit( query[:limit] )
86
+ Person.select( query[:select] )
87
+ .where( query[:where] )
88
+ .order( query[:order] )
89
+ .offset( query[:offset] )
90
+ .limit( query[:limit] )
91
+ .joins( query[:joins] )
73
92
 
74
93
  If you are using WillPaginate, you can also pass in :pagination:
75
94
 
@@ -127,7 +146,7 @@ You can combine multiple strategies, and their results will be merged
127
146
 
128
147
  1. Fork it from https://github.com/claco/muster
129
148
  2. Create your feature branch (`git checkout -b my-new-feature`)
130
- 3. Commit your changes (`git commit -am 'Added some feature'`)
131
- 4. Push to the branch (`git push origin my-new-feature`)
132
- 5. Create new Pull Request
133
-
149
+ 3. Added an entry in Changes/Authors
150
+ 4. Commit your changes (`git commit -am 'Added some feature'`)
151
+ 5. Push to the branch (`git push origin my-new-feature`)
152
+ 6. Create new Pull Request
@@ -5,6 +5,7 @@ require 'muster/results'
5
5
  require 'muster/strategies/filter_expression'
6
6
  require 'muster/strategies/pagination'
7
7
  require 'muster/strategies/sort_expression'
8
+ require 'muster/strategies/joins_expression'
8
9
 
9
10
  module Muster
10
11
  module Strategies
@@ -36,11 +37,12 @@ module Muster
36
37
  pagination = self.parse_pagination( query_string )
37
38
 
38
39
  parameters = Muster::Results.new(
39
- :select => self.parse_select(query_string),
40
- :order => self.parse_order(query_string),
41
- :limit => pagination[:limit],
42
- :offset => pagination[:offset],
43
- :where => self.parse_where(query_string)
40
+ :select => self.parse_select(query_string),
41
+ :order => self.parse_order(query_string),
42
+ :limit => pagination[:limit],
43
+ :offset => pagination[:offset],
44
+ :where => self.parse_where(query_string),
45
+ :joins => self.parse_joins(query_string)
44
46
  )
45
47
 
46
48
  parameters.regular_writer('pagination', pagination[:pagination].symbolize_keys)
@@ -114,6 +116,22 @@ module Muster
114
116
  return results[:where] || {}
115
117
  end
116
118
 
119
+ # Returns joins clauses for AR queries
120
+ #
121
+ # @param query_string [String] the original query string to parse join statements from
122
+ #
123
+ # @return [Hash]
124
+ #
125
+ # @example
126
+ #
127
+ # value = self.parse_joins('joins=authors') #=> {'joins' => 'authors'}
128
+ def parse_joins( query_string )
129
+ strategy = Muster::Strategies::JoinsExpression.new(:field => :joins)
130
+ results = strategy.parse(query_string)
131
+
132
+ return results[:joins] || {}
133
+ end
134
+
117
135
  end
118
136
  end
119
137
  end
@@ -0,0 +1,84 @@
1
+ require 'muster/strategies/hash'
2
+
3
+ module Muster
4
+ module Strategies
5
+
6
+ # Query string parsing strategy with additional value handling options for separating filtering expressions
7
+ #
8
+ # @example
9
+ #
10
+ # strategy = Muster::Strategies::JoinsExpression.new
11
+ # results = strategy.parse('joins=author.name,activity') #=> { 'joins' => [{'author' => 'name'}, 'activity'] }
12
+ class JoinsExpression < Muster::Strategies::Hash
13
+
14
+ # @attribute [r] expression_separator
15
+ # @return [String,RegEx] when specified, each field value will be split into multiple expressions using the specified separator
16
+ attr_reader :expression_separator
17
+
18
+ # @attribute [r] field_separator
19
+ # @return [String,RegEx] when specified, each expression will be split into multiple field/values using the specified separator
20
+ attr_reader :field_separator
21
+
22
+ # Create a new Hash parsing strategy
23
+ #
24
+ # @param [Hash] options the options available for this method
25
+ # @option options [optional,Array<Symbol>] :fields when specified, only parse the specified fields
26
+ # You may also use :field if you only intend to pass one field
27
+ # @option options [optional,String,RegEx] :expression_separator (/,\s*/) when specified, splits the query string value into multiple expressions
28
+ # You may pass the separator as a string or a regular expression
29
+ # @option options [optional,String,RegEx] :field_separator (.) when specified, splits the expression value into multiple field/values
30
+ # You may pass the separator as a string or a regular expression
31
+ # @option options [optional,Boolean] :unique_values (true) when true, ensures field values do not contain duplicates
32
+ #
33
+ # @example
34
+ #
35
+ # strategy = Muster::Strategies::FilterExpression.new
36
+ # strategy = Muster::Strategies::FilterExpression.new(:unique_values => true)
37
+ def initialize( options={} )
38
+ super
39
+
40
+ @expression_separator = self.options.fetch(:expression_separator, /,\s*/)
41
+ @field_separator = self.options.fetch(:field_separator, '.')
42
+ @unique_values = self.options.fetch(:unique_values, true)
43
+ end
44
+
45
+ # Processes a query string and returns an array of hashes that represent an ActiveRecord joins expression
46
+ #
47
+ # @param query_string [String] the query string to parse
48
+ #
49
+ # @return [Muster::Results]
50
+ #
51
+ # @example
52
+ #
53
+ # results = strategy.parse('joins=author.name,activity') #=> { 'joins' => [{'author' => 'name'}, 'activity'] }
54
+ def parse( query_string )
55
+ parameters = Muster::Results.new( self.fields_to_parse(query_string) )
56
+
57
+ parameters.each do |key, value|
58
+ value = value.uniq.first if self.unique_values == true && value.instance_of?(Array)
59
+ parameters[key] = self.make_nested_hash(value)
60
+ end
61
+ end
62
+
63
+ protected
64
+ # Converts the array that represents the value to a nested hash
65
+ #
66
+ # @param value [Array] the value to convert
67
+ #
68
+ # @return [String,Array] An array of nested a Hash / Hashes
69
+ #
70
+ # @example
71
+ #
72
+ # value = self.make_nested_hash('activity,author.country.name') #=> ['activity', {'author' => {'country' => 'name'}}]
73
+ def make_nested_hash( value )
74
+ expressions = value.split(expression_separator)
75
+ expressions.map do |e|
76
+ fields = e.split(field_separator)
77
+ fields[0..-2].reverse.reduce(fields.last) { |a, n| { n => a } }
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+
@@ -1,4 +1,4 @@
1
1
  module Muster
2
2
  # Current version of Muster
3
- VERSION = "0.0.6"
3
+ VERSION = "0.0.7"
4
4
  end
@@ -6,7 +6,7 @@ describe Muster::Strategies::ActiveRecord do
6
6
 
7
7
  describe '#parse' do
8
8
  it 'returns a Muster::Results instance' do
9
- subject.parse('').should == {"select"=>[], "order"=>[], "limit"=>30, "offset"=>nil, "where"=>{}, "pagination"=>{:page=>1, :per_page=>30}}
9
+ subject.parse('').should == {"select"=>[], "order"=>[], "limit"=>30, "offset"=>nil, "where"=>{}, "joins"=>{}, "pagination"=>{:page=>1, :per_page=>30}}
10
10
  subject.parse('').should be_an_instance_of(Muster::Results)
11
11
  end
12
12
 
@@ -49,6 +49,24 @@ describe Muster::Strategies::ActiveRecord do
49
49
  end
50
50
  end
51
51
 
52
+ context 'joins' do
53
+ it 'returns single value in Array' do
54
+ subject.parse('joins=author')[:joins].should eq ['author']
55
+ end
56
+
57
+ it 'returns multiple values in Array' do
58
+ subject.parse('joins=author,voter')[:joins].should eq ['author', 'voter']
59
+ end
60
+
61
+ it 'returns a nested hash of separated values' do
62
+ subject.parse('joins=author.country.name')[:joins].should eq [{'author' => { 'country' => 'name'}}]
63
+ end
64
+
65
+ it 'returns an array of nested hashes' do
66
+ subject.parse('joins=author.country.name,activity.rule')[:joins].should eq [{'author' => { 'country' => 'name'}}, {'activity' => 'rule'}]
67
+ end
68
+ end
69
+
52
70
  context 'pagination' do
53
71
  it 'returns default will paginate compatible pagination' do
54
72
  subject.parse('')[:pagination].should == {:page => 1, :per_page => 30}
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+
3
+ describe Muster::Strategies::JoinsExpression do
4
+ let(:options) { {} }
5
+ subject { Muster::Strategies::JoinsExpression.new(options) }
6
+
7
+ describe '#parse' do
8
+
9
+ context 'by default' do
10
+ it 'returns empty hash for empty query string' do
11
+ subject.parse('').should == {}
12
+ subject.parse('').should be_an_instance_of(Muster::Results)
13
+ end
14
+
15
+ it 'returns hash of all key/value pairs' do
16
+ subject.parse('joins=author,author.foop').should eq({ "joins" => ["author", {"author" => "foop"}] })
17
+ end
18
+
19
+ it 'hash supports indifferent key access' do
20
+ hash = subject.parse('joins=author,activity')
21
+ hash[:joins][0].should eq('author')
22
+ hash[:joins][1].should eq('activity')
23
+ end
24
+
25
+ it 'combines multiple expressions into an array' do
26
+ subject.parse('joins=author,activity').should == { 'joins' => ['author', 'activity'] }
27
+ end
28
+
29
+ it 'discards non unique values' do
30
+ subject.parse('joins=author&joins=author').should == { 'joins' => ['author'] }
31
+ end
32
+ end
33
+
34
+ context 'with :expression_separator option' do
35
+ context 'as regex' do
36
+ before do
37
+ options[:expression_separator] = /\|\s*/
38
+ end
39
+
40
+ it 'converts comma separated value into Array' do
41
+ subject.parse('joins=author|activity').should == { 'joins' => ['author', 'activity'] }
42
+ end
43
+
44
+ it 'ignores spaces after a separator' do
45
+ subject.parse('joins=author|%20 activity').should == { 'joins' => ['author', 'activity'] }
46
+ end
47
+ end
48
+
49
+ context 'as string' do
50
+ before do
51
+ options[:expression_separator] = '|'
52
+ end
53
+
54
+ it 'converts comma separated value into Array' do
55
+ subject.parse('joins=author|activity|rule').should == { 'joins' => ['author', 'activity', 'rule'] }
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'with :field_separator option' do
61
+ context 'as regex' do
62
+ before { options[:field_separator] = /\s*:\s*/ }
63
+
64
+ it 'splits field from values' do
65
+ subject.parse('joins=author:country:name').should == { 'joins' => [{'author' => {'country' => 'name'}}] }
66
+ end
67
+
68
+ it 'ignores spaces after field' do
69
+ subject.parse('joins=author : country').should == { 'joins' => [{'author' => 'country'}] }
70
+ end
71
+ end
72
+
73
+ context 'as string' do
74
+ before { options[:field_separator] = ':' }
75
+
76
+ it 'converts comma separated value into Array' do
77
+ subject.parse('joins=author:country').should == { 'joins' => [{'author' => 'country'}] }
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'with :fields option' do
83
+ context 'as symbol' do
84
+ before { options[:field] = :includes }
85
+
86
+ it 'fields returns expressions for the key specified' do
87
+ subject.parse('includes=author').should == { 'includes' => ['author'] }
88
+ end
89
+ end
90
+
91
+ context 'as Array of symbols' do
92
+ before { options[:fields] = [:includes, :joins] }
93
+
94
+ it 'fields returns expressions for the keys specified' do
95
+ subject.parse('joins=author&includes=activity').should == { 'joins' => ['author'], 'includes' => ['activity'] }
96
+ end
97
+ end
98
+
99
+ context 'as string' do
100
+ before { options[:field] = 'includes' }
101
+
102
+ it 'fields returns expressions for the key specified' do
103
+ subject.parse('includes=author').should == { 'includes' => ['author'] }
104
+ end
105
+ end
106
+
107
+ context 'as Array of strings' do
108
+ before { options[:fields] = ['includes', 'joins'] }
109
+
110
+ it 'fields returns expressions for the keys specified' do
111
+ subject.parse('includes=author&joins=activity').should == { 'includes' => ['author'], 'joins' => ['activity'] }
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: muster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-31 00:00:00.000000000 Z
12
+ date: 2013-03-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -134,6 +134,7 @@ files:
134
134
  - .gitignore
135
135
  - .rspec
136
136
  - .yardopts
137
+ - Authors
137
138
  - Changes
138
139
  - Gemfile
139
140
  - LICENSE
@@ -146,6 +147,7 @@ files:
146
147
  - lib/muster/strategies/active_record.rb
147
148
  - lib/muster/strategies/filter_expression.rb
148
149
  - lib/muster/strategies/hash.rb
150
+ - lib/muster/strategies/joins_expression.rb
149
151
  - lib/muster/strategies/pagination.rb
150
152
  - lib/muster/strategies/rack.rb
151
153
  - lib/muster/strategies/sort_expression.rb
@@ -156,6 +158,7 @@ files:
156
158
  - spec/muster/strategies/active_record_spec.rb
157
159
  - spec/muster/strategies/filter_expression_spec.rb
158
160
  - spec/muster/strategies/hash_spec.rb
161
+ - spec/muster/strategies/joins_expression_spec.rb
159
162
  - spec/muster/strategies/pagination_spec.rb
160
163
  - spec/muster/strategies/sort_expression_spec.rb
161
164
  - spec/spec_helper.rb
@@ -189,6 +192,7 @@ test_files:
189
192
  - spec/muster/strategies/active_record_spec.rb
190
193
  - spec/muster/strategies/filter_expression_spec.rb
191
194
  - spec/muster/strategies/hash_spec.rb
195
+ - spec/muster/strategies/joins_expression_spec.rb
192
196
  - spec/muster/strategies/pagination_spec.rb
193
197
  - spec/muster/strategies/sort_expression_spec.rb
194
198
  - spec/spec_helper.rb