muster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/array/wrap'
3
+ require 'muster/strategies/rack'
4
+
5
+ module Muster
6
+ module Strategies
7
+
8
+ # Query string parsing strategy with additional value handling options for separating values and uniqueness
9
+ #
10
+ # @example
11
+ #
12
+ # strategy = Muster::Strategies::Hash.new(:unique_values => true, :value_separator => ',')
13
+ # results = strategy.parse('name=value&choices=1,2,1') #=> { 'name' => 'value', 'choices' => ['1', '2'] }
14
+ class Hash < Muster::Strategies::Rack
15
+
16
+ # @attribute [r] value_separator
17
+ # @return [String,RegEx] when specified, each field value will be split into multiple values using the specified separator
18
+ attr_reader :value_separator
19
+
20
+ # @attribute [r] unique_values
21
+ # @return [Boolean] when specified, ensures a fields values do not contain duplicates
22
+ attr_reader :unique_values
23
+
24
+ # Create a new Hash parsing strategy
25
+ #
26
+ # @param [Hash] options the options available for this method
27
+ # @option options [optional,Array<Symbol>] :fields when specified, only parse the specified fields
28
+ # You may also use :field if you only intend to pass one field
29
+ # @option options [optional,String,RegEx] :value_separator (/,\s*/) when specified, splits the field value into multiple 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::Hash.new(:fields => [:name, :state], :value_separator => '|')
36
+ # strategy = Muster::Strategies::Hash.new(:unique_values => false)
37
+ def initialize( options={} )
38
+ super
39
+
40
+ @unique_values = self.options.fetch(:unique_values, true)
41
+ @value_separator = self.options.fetch(:value_separator, /,\s*/)
42
+ end
43
+
44
+ # Processes a query string and returns a hash of its fields/values
45
+ #
46
+ # @param query_string [String] the query string to parse
47
+ #
48
+ # @return [Hash]
49
+ #
50
+ # @example
51
+ #
52
+ # results = strategy.parse('name=value&choices=1,2') #=> { 'name' => 'value', 'choices' => ['1', '2'] }
53
+ def parse( query_string )
54
+ parameters = super
55
+
56
+ parameters.each do |key, value|
57
+ if self.value_separator.present?
58
+ parameters[key] = self.separate_values(value)
59
+ end
60
+
61
+ if self.unique_values == true && value.instance_of?(Array)
62
+ parameters[key].uniq!
63
+ end
64
+ end
65
+
66
+ return parameters
67
+ end
68
+
69
+ protected
70
+
71
+ # Separates values into an Array of values using :values_separator
72
+ #
73
+ # @param value [String,Array] the original query string field value to separate
74
+ #
75
+ # @return [String,Array] String if a single value exists, Array otherwise
76
+ #
77
+ # @example
78
+ #
79
+ # value = self.separate_values('1') #=> '1'
80
+ # value = self.separate_values('1,2') #=> ['1', '2']
81
+ def separate_values( value )
82
+ values = Array.wrap(value)
83
+
84
+ values = values.map do |value|
85
+ value.split(self.value_separator)
86
+ end.flatten
87
+
88
+ return (values.size > 1) ? values : value
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,111 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'muster/strategies/hash'
3
+
4
+ module Muster
5
+ module Strategies
6
+
7
+ # Query string parsing strategy with logic to handle pagination options
8
+ #
9
+ # @example
10
+ #
11
+ # strategy = Muster::Strategies::Pagination.new
12
+ # results = strategy.parse('page=3&per_page=10') #=> { 'pagination' => {'page' => 3, 'per_page' => 10}, 'limit' => 10, 'offset' => 20 }
13
+ class Pagination < Muster::Strategies::Rack
14
+
15
+ # @attribute [r] default_page_size
16
+ # @return [Fixnum] when specified, will override the default page size of 30 when no page_size is parsed
17
+ attr_accessor :default_page_size
18
+
19
+ # Create a new Pagination parsing strategy
20
+ #
21
+ # @param [Hash] options the options available for this method
22
+ # @option options [optional,Array<Symbol>] :fields when specified, only parse the specified fields
23
+ # You may also use :field if you only intend to pass one field
24
+ # @option options [optional,String,RegEx] :value_separator (/,\s*/) when specified, splits the field value into multiple values
25
+ # You may pass the separator as a string or a regular expression
26
+ # @option options [optional,Boolean] :unique_values (true) when true, ensures field values do not contain duplicates
27
+ # @option options [options,Fixnum] :default_page_size (30) when specified, the default page size to use when no page size is parsed
28
+ #
29
+ # @example
30
+ #
31
+ # strategy = Muster::Strategies::Pagination.new
32
+ # strategy = Muster::Strategies::Pagination.new(:default_page_size => 10)
33
+ def initialize( options={} )
34
+ super
35
+
36
+ self.default_page_size = self.options[:default_page_size].to_i
37
+ if self.default_page_size < 1
38
+ self.default_page_size = 30
39
+ end
40
+ end
41
+
42
+ # Processes a query string and returns a hash of its fields/values
43
+ #
44
+ # @param query_string [String] the query string to parse
45
+ #
46
+ # @return [Hash]
47
+ #
48
+ # @example
49
+ #
50
+ # results = strategy.parse('page=3&per_page=10') #=> { 'pagination' => {'page' => 3, 'per_page' => 10}, 'limit' => 10, 'offset' => 20 }
51
+ def parse( query_string )
52
+ parameters = self.parse_query_string(query_string)
53
+
54
+ page = self.parse_page(parameters)
55
+ page_size = self.parse_page_size(parameters)
56
+
57
+
58
+ offset = (page - 1) * page_size
59
+ offset = nil if offset < 1
60
+
61
+ parameters = parameters.merge(:pagination => {:page => page, :per_page => page_size}, :limit => page_size, :offset => offset)
62
+
63
+ if self.fields.present?
64
+ parameters = parameters.slice(*self.fields).with_indifferent_access
65
+ end
66
+
67
+ parameters
68
+ end
69
+
70
+ protected
71
+
72
+ # Returns the current page for the current query string.
73
+ #
74
+ # If page is not specified, or is not a positive number, 1 will be returned instead.
75
+ #
76
+ # @param parameters [Hash] the parameters parsed from the query string
77
+ #
78
+ # @return [Fixnum]
79
+ #
80
+ # @example
81
+ #
82
+ # page = self.parse_page(:page => 2) #=> 2
83
+ # page = self.parse_page(:page => nil) #=> 1
84
+ def parse_page( parameters )
85
+ page = parameters.delete(:page).to_i
86
+ page = 1 unless page > 0
87
+ page
88
+ end
89
+
90
+ # Returns the page size for the current query string.
91
+ #
92
+ # If per_page or page_size is not specified, or is not a positive number, :default_page_size will be returned instead.
93
+ #
94
+ # @param parameters [Hash] the parameters parsed from the query string
95
+ #
96
+ # @return [Fixnum]
97
+ #
98
+ # @example
99
+ #
100
+ # page_size = self.parse_page(:page_size => 10) #=> 10
101
+ # page_size = self.parse_page(:per_page => 10) #=> 10
102
+ # page_size = self.parse_page(:per_page => nil) #=> 30
103
+ def parse_page_size( parameters )
104
+ page_size = (parameters.delete(:page_size) || parameters.delete(:per_page)).to_i
105
+ page_size = self.default_page_size unless page_size > 0
106
+ page_size
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,89 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+ require 'active_support/core_ext/array/wrap'
4
+ require 'rack/utils'
5
+
6
+ module Muster
7
+ module Strategies
8
+
9
+ # Query string parsing strategy based on Rack::Utils#parse_query
10
+ #
11
+ # @example
12
+ #
13
+ # strategy = Muster::Strategies::Rack.new
14
+ # results = strategy.parse('name=value&choices=1&choices=2') #=> { 'name' => 'value', 'choices' => ['1', '2'] }
15
+ class Rack
16
+
17
+ # @attribute [r] options
18
+ # @return [Hash] options specified during initialization
19
+ attr_reader :options
20
+
21
+ # @attribute [r] fields
22
+ # @return [Hash] list of fields to parse, ignoring all others
23
+ attr_reader :fields
24
+
25
+ # Create a new Rack parsing strategy
26
+ #
27
+ # @param [Hash] options the options available for this method
28
+ # @option options [optional,Array<Symbol>] :fields when specified, only parse the specified fields
29
+ # You may also use :field if you only intend to pass one field
30
+ #
31
+ # @example
32
+ #
33
+ # strategy = Muster::Strategies::Rack.new(:fields => [:name, :state])
34
+ # strategy = Muster::Strategies::Rack.new(:field => :name)
35
+ def initialize( options={} )
36
+ @options = options.with_indifferent_access
37
+
38
+ @fields = Array.wrap(@options[:field] || @options[:fields])
39
+ @fields.map!{ |field| field.to_sym }
40
+ end
41
+
42
+ # Processes a query string and returns a hash of its fields/values
43
+ #
44
+ # @param query_string [String] the query string to parse
45
+ #
46
+ # @return [Hash]
47
+ #
48
+ # @example
49
+ #
50
+ # results = strategy.parse('name=value&choices=1&choices=1') #=> { 'name' => 'value', 'choices' => ['1', '2'] }
51
+ def parse( query_string )
52
+ self.fields_to_parse(query_string)
53
+ end
54
+
55
+ protected
56
+
57
+ # Converts the query string into a hash for processing
58
+ #
59
+ # @param query_string [String] the query string being parsed
60
+ #
61
+ # @return [Hash]
62
+ #
63
+ # @example
64
+ #
65
+ # fields = self.parse_query_string('name=value&choices=1&choices=1') #=> { 'name' => 'value', 'choices' => ['1', '2'] }
66
+ def parse_query_string( query_string )
67
+ ::Rack::Utils.parse_query(query_string).with_indifferent_access
68
+ end
69
+
70
+ # Returns the list of fields to be parsed
71
+ #
72
+ # @param query_string [String] the query string to parse
73
+ #
74
+ # If the :fields option was specified, only those fields will be returned. Otherwise, all fields will be returned.
75
+ #
76
+ # @return [Hash]
77
+ def fields_to_parse( query_string )
78
+ fields = self.parse_query_string(query_string)
79
+
80
+ if self.fields.present?
81
+ fields = fields.select{ |key, value| self.fields.include?(key.to_sym) }
82
+ end
83
+
84
+ return fields.with_indifferent_access
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,75 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'muster/strategies/hash'
3
+
4
+ module Muster
5
+ module Strategies
6
+
7
+ # Query string parsing strategy with additional value handling for sort orders
8
+ #
9
+ # @example
10
+ #
11
+ # strategy = Muster::Strategies::SortExpression.new
12
+ # results = strategy.parse('sort=name:desc') #=> { 'sort' => 'name desc' }
13
+ class SortExpression < Muster::Strategies::Hash
14
+
15
+ # Processes a query string and returns a hash of its fields/values
16
+ #
17
+ # @param query_string [String] the query string to parse
18
+ #
19
+ # @return [Hash]
20
+ #
21
+ # @example
22
+ #
23
+ # results = strategy.parse('order=name') #=> { 'order' => 'name asc' }
24
+ # results = strategy.parse('order=name:desc') #=> { 'order' => 'name desc' }
25
+ # results = strategy.parse('order=name,date:desc') #=> { 'order' => ['name asc', 'date desc'] }
26
+ def parse( query_string )
27
+ parameters = super
28
+
29
+ parameters.each do |key, value|
30
+ parameters[key] = self.parse_sort_expression(value)
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ # Separates the values into their field and direction
37
+ #
38
+ # @param value [String] the value being parsed
39
+ #
40
+ # @return [String,Arrary] String if a single value, Array otherwise
41
+ #
42
+ # @example
43
+ #
44
+ # value = self.parse_sort_expression('name:asc') #=> 'name asc'
45
+ # value = self.parse_sort_expression(['name:asc', 'date']) #=> ['name asc', 'date asc']
46
+ def parse_sort_expression( value )
47
+ values = Array.wrap(value)
48
+
49
+ values = values.map do |value|
50
+ name, direction = value.split(':', 2)
51
+ direction = self.parse_direction(direction)
52
+
53
+ "#{name} #{direction}"
54
+ end.flatten
55
+
56
+ return (values.size > 1) ? values : values.first
57
+ end
58
+
59
+ # Parse and normalize the sot expression direction
60
+ #
61
+ # @param direction [String] the direction to normalize
62
+ #
63
+ # @return [String]
64
+ #
65
+ # @example
66
+ #
67
+ # direction = self.parse_direction('ascending') #=> 'asc'
68
+ # direction = self.parse_direction('descending') #=> 'desc'
69
+ def parse_direction( direction )
70
+ direction.to_s.match(/^desc/i) ? 'desc' : 'asc'
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module Muster
2
+ # Current version of Muster
3
+ VERSION = "0.0.1"
4
+ end
data/muster.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/muster/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Christopher H. Laco"]
6
+ gem.email = ["claco@chrislaco.com"]
7
+ gem.description = %q{Muster is a gem that turns query string options in varying formats into data structures suitable for use in AR scopes and queryies.}
8
+ gem.summary = %q{Muster various query string options into AR query compatable options.}
9
+ gem.homepage = "https://github.com/claco/muster"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "muster"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Muster::VERSION
17
+
18
+ gem.add_dependency 'activesupport', '~> 3.0'
19
+ gem.add_dependency 'rack', '~> 1.4'
20
+
21
+ gem.add_development_dependency 'rspec', '~> 2.10.0'
22
+ gem.add_development_dependency 'redcarpet', '~> 2.1'
23
+ gem.add_development_dependency 'yard', '~> 0.8.2'
24
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Muster::Rack do
4
+ let(:application) { lambda{|env| [200, {'Content-Type' => 'text/plain'}, '']} }
5
+ let(:environment) { Rack::MockRequest.env_for('/?name=value&order=name') }
6
+ let(:options) { {} }
7
+ let(:middleware) { Muster::Rack.new(application, Muster::Strategies::Hash, options) }
8
+
9
+ it 'parse query string with strategy' do
10
+ middleware.call(environment)
11
+
12
+ environment[Muster::Rack::QUERY].should == {'name' => 'value', 'order' => 'name'}
13
+ end
14
+
15
+ it 'accepts options for middlewhere' do
16
+ options[:field] = :name
17
+
18
+ middleware.call(environment)
19
+
20
+ environment[Muster::Rack::QUERY].should == {'name' => 'value'}
21
+ end
22
+
23
+ it 'accepts a strategy instance' do
24
+ strategy = Muster::Strategies::Hash.new(:field => :name)
25
+ Muster::Rack.new(application, strategy).call(environment)
26
+
27
+ environment[Muster::Rack::QUERY].should == {'name' => 'value'}
28
+ end
29
+
30
+ it 'merges multiple strategies into one result' do
31
+ Muster::Rack.new(application, Muster::Strategies::Hash, :field => :name).call(environment)
32
+ environment[Muster::Rack::QUERY].should == {'name' => 'value'}
33
+
34
+ Muster::Rack.new(application, Muster::Strategies::Hash, :field => :order).call(environment)
35
+ environment[Muster::Rack::QUERY].should == {'name' => 'value', 'order' => 'name'}
36
+ end
37
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ describe Muster::Strategies::ActiveRecord do
4
+ let(:options) { {} }
5
+ subject { Muster::Strategies::ActiveRecord.new(options) }
6
+
7
+ describe '#parse' do
8
+ context 'selects' do
9
+ it 'returns single value as Array' do
10
+ subject.parse('select=id')[:select].should == ['id']
11
+ end
12
+
13
+ it 'returns values in Array' do
14
+ subject.parse('select=id&select=name')[:select].should == ['id', 'name']
15
+ end
16
+
17
+ it 'supports comma separated values' do
18
+ subject.parse('select=id&select=guid,name')[:select].should == ['id', 'guid', 'name']
19
+
20
+ end
21
+ end
22
+
23
+ context 'orders' do
24
+ it 'returns single value as Array' do
25
+ subject.parse('order=id')[:order].should == ['id asc']
26
+ end
27
+
28
+ context 'with direction' do
29
+ it 'supports asc' do
30
+ subject.parse('order=id:asc')[:order].should == ['id asc']
31
+ end
32
+
33
+ it 'supports desc' do
34
+ subject.parse('order=id:desc')[:order].should == ['id desc']
35
+ end
36
+
37
+ it 'supports ascending' do
38
+ subject.parse('order=id:ascending')[:order].should == ['id asc']
39
+ end
40
+
41
+ it 'supports desc' do
42
+ subject.parse('order=id:descending')[:order].should == ['id desc']
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'pagination' do
48
+ it 'returns default will paginate compatible pagination' do
49
+ subject.parse('')[:pagination].should == {:page => 1, :per_page => 30}
50
+ end
51
+
52
+ it 'returns default limit options' do
53
+ subject.parse('')[:limit].should eq 30
54
+ end
55
+
56
+ it 'returns default offset options' do
57
+ subject.parse('')[:offset].should eq nil
58
+ end
59
+
60
+ it 'accepts per_page option' do
61
+ results = subject.parse('per_page=10')
62
+ results[:pagination].should == {:page => 1, :per_page => 10}
63
+ results[:limit].should eq 10
64
+ results[:offset].should eq nil
65
+ end
66
+
67
+ it 'ensures per_page is positive integer' do
68
+ results = subject.parse('per_page=-10')
69
+ results[:pagination].should == {:page => 1, :per_page => 30}
70
+ results[:limit].should eq 30
71
+ results[:offset].should eq nil
72
+ end
73
+
74
+ it 'accepts page_size option' do
75
+ results = subject.parse('page_size=10')
76
+ results[:pagination].should == {:page => 1, :per_page => 10}
77
+ results[:limit].should eq 10
78
+ results[:offset].should eq nil
79
+ end
80
+
81
+ it 'accepts page option' do
82
+ results = subject.parse('page=2')
83
+ results[:pagination].should == {:page => 2, :per_page => 30}
84
+ results[:limit].should eq 30
85
+ results[:offset].should eq 30
86
+ end
87
+
88
+ it 'ensures page is positive integer' do
89
+ results = subject.parse('page=a')
90
+ results[:pagination].should == {:page => 1, :per_page => 30}
91
+ results[:limit].should eq 30
92
+ results[:offset].should eq nil
93
+ end
94
+ end
95
+
96
+ context 'wheres' do
97
+ it 'returns a single value as a string in a hash' do
98
+ subject.parse('where=id:1')[:where].should == {'id' => '1'}
99
+ end
100
+
101
+ it 'returns values as an Array in a hash' do
102
+ subject.parse('where=id:1&where=id:2')[:where].should == {'id' => ['1', '2']}
103
+ end
104
+
105
+ it 'supports pipe for multiple values' do
106
+ subject.parse('where=id:1|2')[:where].should == {'id' => ['1', '2']}
107
+ end
108
+ end
109
+
110
+ context 'the full monty' do
111
+ it 'returns a hash of all options' do
112
+ query_string = 'select=id,guid,name&where=name:foop&order=id:desc&order=name&page=3&page_size=5'
113
+ results = subject.parse(query_string)
114
+
115
+ results[:select].should == ['id', 'guid', 'name']
116
+ results[:where].should == {'name' => 'foop'}
117
+ results[:order].should == ['id desc', 'name asc']
118
+ results[:pagination].should == {:page => 3, :per_page => 5}
119
+ results[:offset].should == 10
120
+ results[:limit].should == 5
121
+ end
122
+
123
+ it 'supports indifferent access' do
124
+ query_string = 'select=id,guid,name&where=name:foop&order=id:desc&order=name&page=3&page_size=5'
125
+ results = subject.parse(query_string)
126
+
127
+ results['select'].should == ['id', 'guid', 'name']
128
+ results['where'].should == {'name' => 'foop'}
129
+ results['order'].should == ['id desc', 'name asc']
130
+ results['pagination'].should == {:page => 3, :per_page => 5}
131
+ results['offset'].should == 10
132
+ results['limit'].should == 5
133
+
134
+ end
135
+ end
136
+ end
137
+ end