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