muster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rvmrc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --protected
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in muster.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Christopher H. Laco
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Muster
2
+
3
+ Muster is a gem that turns query string options of varying formats into data structures suitable for
4
+ easier consumption in things like AR and DataMapper scopes and queries, making API development just a little bit easier.
5
+
6
+ require 'muster'
7
+
8
+ use Muster::Rack, Muster::Strategies::ActiveRecord, :fields => [:select, :order]
9
+
10
+ # GET /people?select=id,name&order=name:desc
11
+ # in your routes/controllers
12
+ query = env['muster.query']
13
+
14
+ @people = Person.select( query[:select] ).order( query[:order] )
15
+
16
+ ## Strategies
17
+
18
+ The following types of strategies are supported. You can combine them in Rack any way you see fit to match the query string options
19
+ you want to support.
20
+
21
+ ### Rack
22
+
23
+ Returns the query as parsed by Rack::Utils#parse_query.
24
+
25
+ ?name=value1&name=value2&choice=1&choice=1
26
+
27
+ { 'name' => ['value1', 'value2'], 'choice' => ['1', '1'] }
28
+
29
+ ### Hash
30
+
31
+ Same as Rack but with support for delimited value separation and unique value detection.
32
+
33
+ ?name=value1,value2&choice=1&choice=1
34
+
35
+ { 'name' => ['value1', 'value2'], 'choice' => '1' }
36
+
37
+ ### FilterExpression
38
+
39
+ Allows name value pairs to be specified in the query string values for use in filtering methods that take Hashes.
40
+ Include delimited value and unique value support from above.
41
+
42
+ ?filter=id:1&filter=name:food #=> { 'filter' => {'id' => '1', 'name' => 'food'} }
43
+ ?filter=id:1&filter=id:2 #=> { 'filter' => {'id' => ['1', '2']} }
44
+ ?filter=id:1|2 #=> { 'filter' => {'id' => ['1', '2']} }
45
+ ?filter=id:1,name:food #=> { 'filter' => {'id' => '1', 'name' => 'food'} }
46
+
47
+ ### SortExpression
48
+
49
+ Returns options with support for dirctional indicators for use in sorting.
50
+
51
+ ?order=name&order=age #=> { 'order' => ['name asc', 'age asc'] }
52
+ ?order=name:asc&age:desc #=> { 'order' => ['name asc', 'age desc'] }
53
+
54
+ ### Pagination
55
+
56
+ Returns options to support pagination with logic for default page size, not-a-number checks and offset calculations.
57
+
58
+ ?page=2&per_page=5 #=> { 'pagination' => {'page' => 2, 'per_page' => 5}, 'limit' => 5, 'offset' => 5 }
59
+ ?page=a&page_size=-2 #=> { 'pagination' => {'page' => 1, 'per_page' => 30}, 'limit' => 30, 'offset' => nil}
60
+
61
+
62
+ ### ActiveRecord
63
+
64
+ Combines many of the strategies above to output ActiveRecord Query interface compatible options.
65
+
66
+ ?select=id,name&where=status:new&order=name:desc&page=3&page_size=10
67
+
68
+ { 'select' => ['id', 'name'], 'where' => {'status' => 'new'}, 'order' => 'name desc', 'limit' => 10, 'offset' => 20, 'pagination' => {:page => 3, :per_page => 10} }
69
+
70
+ query = env['muster.query']
71
+ Person.select( query[:select] ).where( query[:where] ).order( query[:order] ).offset( query[:offset] ).limit( query[:limit] )
72
+
73
+ If you are using WillPaginate, you can also pass in :pagination:
74
+
75
+ Person.paginate( query[:paginate] )
76
+
77
+ ## Installation
78
+
79
+ Add this line to your application's Gemfile:
80
+
81
+ gem 'muster'
82
+
83
+ And then execute:
84
+
85
+ $ bundle
86
+
87
+ Or install it yourself as:
88
+
89
+ $ gem install muster
90
+
91
+ ## Usage
92
+
93
+ ### In any Rack application:
94
+
95
+ require 'muster'
96
+
97
+ use Muster::Rack, Muster::Strategies::ActiveRecord, :fields => [:select, :order]
98
+
99
+ # GET /people?select=id,name&order=name:desc
100
+ # in your routes/controllers
101
+ query = env['muster.query']
102
+
103
+ @people = Person.select( query[:select] ).order( query[:order] )
104
+
105
+ You can combine multiple strategies, and their results will be merged
106
+
107
+ use Muster::Rack, Muster::Strategies::Hash, :field => :select
108
+ use Muster::Rack, Muster::Strategies::ActiveRecord, :field => :order
109
+
110
+ # GET /people?select=id,name&order=name:desc
111
+ # in your routes/controllers
112
+ query = env['muster.query']
113
+
114
+ @people = Person.select( query[:select] ).order( query[:order] )
115
+
116
+
117
+ ### In any code:
118
+
119
+ require 'muster'
120
+
121
+ strategy = Muster::Strategies::Hash.new
122
+ query = strategy.parse(request.query_string)
123
+ people = Person.select( query[:select] ).order( query[:order] )
124
+
125
+ ## Contributing
126
+
127
+ 1. Fork it from https://github.com/claco/muster
128
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
129
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
130
+ 4. Push to the branch (`git push origin my-new-feature`)
131
+ 5. Create new Pull Request
132
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+ require 'yard/rake/yardoc_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ YARD::Rake::YardocTask.new(:yard)
data/lib/muster.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'muster/version'
2
+ require 'muster/strategies'
3
+ require 'muster/rack'
@@ -0,0 +1,72 @@
1
+ require 'rack'
2
+
3
+ module Muster
4
+
5
+ # Rack middleware plugin for Muster query string parsing
6
+ #
7
+ # @example
8
+ #
9
+ # app = Rack::Builder.new do
10
+ # use Rack::Muster, Muster::Strategies::Hash, :fields => [:name, :choices]
11
+ # end
12
+ #
13
+ # # GET /?name=bob&choices=1&choices=2
14
+ # match '/' do
15
+ # env['muster.query'] #=> {'name' => 'bob', 'choices' => ['1', '2']}
16
+ # end
17
+ class Rack
18
+
19
+ # @attribute [r] app
20
+ # @return [Object] Rack application middleware is running under
21
+ attr_reader :app
22
+
23
+ # @attribute [r] strategy
24
+ # @return [Muster::Strategies::Rack] Muster Strategy to run
25
+ attr_reader :strategy
26
+
27
+ # @attribute [r] options
28
+ # @return [Hash] options to pass to strategy
29
+ attr_reader :options
30
+
31
+ # Key in ENV where processed query string are stored
32
+ QUERY = 'muster.query'.freeze
33
+
34
+ # Key in ENV where the query string that was processed is stored
35
+ QUERY_STRING = 'muster.query_string'.freeze
36
+
37
+ # Creates a new Rack::Muster middleware instance
38
+ #
39
+ # @param app [String] Rack application
40
+ # @param strategy [Muster::Strategies::Rack] Muster query string parsing strategy to run
41
+ # @param options [Hash] options to pass to the specified strategy
42
+ #
43
+ # @example
44
+ #
45
+ # middleware = Muster::Rack.new(app, Muster::Strategies::Hash, :fields => [:name, :choices])
46
+ #
47
+ # strategy = Muster::Strategies::Hash.new(:fields => [:name, :choices])
48
+ # middleware = Muster::Rack.new(app, strategy)
49
+ def initialize( app, strategy, options={} )
50
+ @app = app
51
+ @strategy = strategy
52
+ @options = options
53
+ end
54
+
55
+ # Handle Rack request
56
+ #
57
+ # @param env [Hash] Rack environment
58
+ #
59
+ # @return [Array]
60
+ def call( env )
61
+ request = ::Rack::Request.new(env)
62
+ parser = self.strategy.kind_of?(Class) ? self.strategy.new(options) : self.strategy
63
+
64
+ env[QUERY] ||= {}
65
+ env[QUERY].merge! parser.parse(request.query_string)
66
+ env[QUERY_STRING] = request.query_string
67
+
68
+ @app.call(env)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,6 @@
1
+ require 'muster/strategies/rack.rb'
2
+ require 'muster/strategies/active_record'
3
+ require 'muster/strategies/filter_expression'
4
+ require 'muster/strategies/hash'
5
+ require 'muster/strategies/pagination'
6
+ require 'muster/strategies/sort_expression'
@@ -0,0 +1,118 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'muster/strategies/hash'
4
+ require 'muster/strategies/filter_expression'
5
+ require 'muster/strategies/pagination'
6
+ require 'muster/strategies/sort_expression'
7
+
8
+ module Muster
9
+ module Strategies
10
+
11
+ # Query string parsing strategy that outputs ActiveRecord Query compatible options
12
+ #
13
+ # @example
14
+ #
15
+ # strategy = Muster::Strategies::ActiveRecord.new
16
+ # results = strategy.parse('select=id,name&where=status:new&order=name:desc')
17
+ #
18
+ # # { 'select' => ['id', 'name'], :where => {'status' => 'new}, :order => 'name desc' }
19
+ # #
20
+ # # Person.select( results[:select] ).where( results[:where] ).order( results[:order] )
21
+ class ActiveRecord < Muster::Strategies::Rack
22
+
23
+ # Processes a query string and returns a hash of its fields/values
24
+ #
25
+ # @param query_string [String] the query string to parse
26
+ #
27
+ # @return [Hash]
28
+ #
29
+ # @example
30
+ #
31
+ # results = strategy.parse('select=id,name&where=status:new&order=name:desc')
32
+ #
33
+ # # { 'select' => ['id', 'name'], :where => {'status' => 'new}, :order => 'name desc' }
34
+ def parse( query_string )
35
+ pagination = self.parse_pagination( query_string )
36
+
37
+ parameters = ActiveSupport::HashWithIndifferentAccess.new(
38
+ :select => self.parse_select(query_string),
39
+ :order => self.parse_order(query_string),
40
+ :limit => pagination[:limit],
41
+ :offset => pagination[:offset],
42
+ :where => self.parse_where(query_string)
43
+ )
44
+
45
+ parameters.regular_writer('pagination', pagination[:pagination].symbolize_keys)
46
+
47
+ return parameters
48
+ end
49
+
50
+ protected
51
+
52
+ # Returns select clauses for AR queries
53
+ #
54
+ # @param query_string [String] the original query string to parse select clauses from
55
+ #
56
+ # @return [Array]
57
+ #
58
+ # @example
59
+ #
60
+ # value = self.parse_select('select=id,name') #=> ['id', 'name']
61
+ def parse_select( query_string )
62
+ strategy = Muster::Strategies::Hash.new(:field => :select)
63
+ results = strategy.parse(query_string)
64
+
65
+ return Array.wrap( results[:select] )
66
+ end
67
+
68
+ # Returns order by clauses for AR queries
69
+ #
70
+ # @param query_string [String] the original query string to parse order clauses from
71
+ #
72
+ # @return [Array]
73
+ #
74
+ # @example
75
+ #
76
+ # value = self.parse_order('order=name:desc') #=> ['name asc']
77
+ def parse_order( query_string )
78
+ strategy = Muster::Strategies::SortExpression.new(:field => :order)
79
+ results = strategy.parse(query_string)
80
+
81
+ return Array.wrap( results[:order] )
82
+ end
83
+
84
+ # Returns pagination information for AR queries
85
+ #
86
+ # @param query_string [String] the original query string to parse pagination from
87
+ #
88
+ # @return [Hash]
89
+ #
90
+ # @example
91
+ #
92
+ # value = self.parse_pagination('page=2&page_size=10') #=> { 'pagination' => {:page => 2, :per_page => 10}, 'limit' => 10, 'offset' => 10 }
93
+ def parse_pagination( query_string )
94
+ strategy = Muster::Strategies::Pagination.new(:fields => [:pagination, :limit, :offset])
95
+ results = strategy.parse(query_string)
96
+
97
+ return results
98
+ end
99
+
100
+ # Returns where clauses for AR queries
101
+ #
102
+ # @param query_string [String] the original query string to parse where statements from
103
+ #
104
+ # @return [Hash]
105
+ #
106
+ # @example
107
+ #
108
+ # value = self.parse_where('where=id:1') #=> {'id' => '1'}
109
+ def parse_where( query_string )
110
+ strategy = Muster::Strategies::FilterExpression.new(:field => :where)
111
+ results = strategy.parse(query_string)
112
+
113
+ return results[:where] || {}
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,142 @@
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 options for separating filtering expressions
8
+ #
9
+ # @example
10
+ #
11
+ # strategy = Muster::Strategies::FilterExpression.new
12
+ # results = strategy.parse('where=id:1&name:Bob') #=> { 'where' => {'id' => '1', 'name' => 'Bob'} }
13
+ class FilterExpression < Muster::Strategies::Hash
14
+
15
+ # @attribute [r] expression_separator
16
+ # @return [String,RegEx] when specified, each field value will be split into multiple expressions using the specified separator
17
+ attr_reader :expression_separator
18
+
19
+ # @attribute [r] field_separator
20
+ # @return [String,RegEx] when specified, each expression will be split into multiple field/values using the specified separator
21
+ attr_reader :field_separator
22
+
23
+ # Create a new Hash parsing strategy
24
+ #
25
+ # @param [Hash] options the options available for this method
26
+ # @option options [optional,Array<Symbol>] :fields when specified, only parse the specified fields
27
+ # You may also use :field if you only intend to pass one field
28
+ # @option options [optional,String,RegEx] :expression_separator (/,\s*/) when specified, splits the query string value into multiple expressions
29
+ # You may pass the separator as a string or a regular expression
30
+ # @option options [optional,String,RegEx] :field_separator (:) when specified, splits the expression value into multiple field/values
31
+ # You may pass the separator as a string or a regular expression
32
+ # @option options [optional,String,RegEx] :value_separator (|) when specified, splits the field value into multiple values
33
+ # You may pass the separator as a string or a regular expression
34
+ # @option options [optional,Boolean] :unique_values (true) when true, ensures field values do not contain duplicates
35
+ #
36
+ # @example
37
+ #
38
+ # strategy = Muster::Strategies::FilterExpression.new
39
+ # strategy = Muster::Strategies::FilterExpression.new(:unique_values => false)
40
+ def initialize( options={} )
41
+ super
42
+
43
+ @expression_separator = self.options.fetch(:expression_separator, /,\s*/)
44
+ @field_separator = self.options.fetch(:field_separator, ':')
45
+ @value_separator = self.options.fetch(:value_separator, '|')
46
+ end
47
+
48
+ # Processes a query string and returns a hash of its fields/values
49
+ #
50
+ # @param query_string [String] the query string to parse
51
+ #
52
+ # @return [Hash]
53
+ #
54
+ # @example
55
+ #
56
+ # results = strategy.parse('where=id:1&name:Bob') #=> { 'where' => {'id' => '1', 'name' => 'Bob'} }
57
+ def parse( query_string )
58
+ parameters = self.fields_to_parse(query_string)
59
+
60
+ parameters.each do |key, value|
61
+ parameters[key] = self.separate_expressions(value)
62
+ parameters[key] = self.separate_fields(parameters[key])
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ # Separates values into an Array of expressions using :expression_separator
69
+ #
70
+ # @param value [String,Array] the original query string field value to separate
71
+ #
72
+ # @return [String,Array] String if a single value exists, Array otherwise
73
+ #
74
+ # @example
75
+ #
76
+ # value = self.separate_values('where=id:1') #=> {'where' => 'id:1'}
77
+ # value = self.separate_values('where=id:1,id:2') #=> {'where' => ['id:1', 'id:2']}
78
+ def separate_expressions( value )
79
+ values = Array.wrap(value)
80
+
81
+ values = values.map do |value|
82
+ value.split(self.expression_separator)
83
+ end.flatten
84
+
85
+ return (values.size > 1) ? values : values.first
86
+ end
87
+
88
+ # Separates expression field values into an Hash of expression filters using :field_separator
89
+ #
90
+ # @param value [String,Array] the expressions field value to separate
91
+ #
92
+ # @return [Hash]
93
+ #
94
+ # @example
95
+ #
96
+ # value = self.separate_fields('id:1') #=> {'id' => '1'}
97
+ # value = self.separate_values('id:1|2') #=> {'id' => '1|2'}
98
+ def separate_fields( value )
99
+ values = Array.wrap(value)
100
+
101
+ filters = {}
102
+
103
+ values.each do |value|
104
+ name, value = value.split(self.field_separator, 2)
105
+
106
+ if self.value_separator.present?
107
+ value = self.separate_values(value)
108
+ end
109
+
110
+ filters[name] = filters.has_key?(name) ? [filters[name], value].flatten : value
111
+
112
+ if self.unique_values == true && filters[name].instance_of?(Array)
113
+ filters[name].uniq!
114
+ end
115
+ end
116
+
117
+ return filters
118
+ end
119
+
120
+ # Separates expression filter values into an Array of expression filter values using :value_separator
121
+ #
122
+ # @param value [String,Array] the expressions filter value to separate
123
+ #
124
+ # @return [String,Array] String if a single value exists, Array otherwise
125
+ #
126
+ # @example
127
+ #
128
+ # value = self.separate_values('1') #=> '1'
129
+ # value = self.separate_values('1|2') #=> ['1', '2']
130
+ def separate_values( value )
131
+ values = Array.wrap(value)
132
+
133
+ values = values.map do |value|
134
+ value.split(self.value_separator)
135
+ end.flatten
136
+
137
+ return (values.size > 1) ? values : value
138
+ end
139
+
140
+ end
141
+ end
142
+ end