muster 0.0.1

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.
@@ -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