query_helper 0.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +112 -0
- data/LICENSE.txt +21 -0
- data/README.md +381 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/query_helper/associations.rb +33 -0
- data/lib/query_helper/column_map.rb +56 -0
- data/lib/query_helper/filter.rb +112 -0
- data/lib/query_helper/invalid_query_error.rb +3 -0
- data/lib/query_helper/query_helper_concern.rb +41 -0
- data/lib/query_helper/sql_filter.rb +43 -0
- data/lib/query_helper/sql_manipulator.rb +62 -0
- data/lib/query_helper/sql_parser.rb +189 -0
- data/lib/query_helper/sql_sort.rb +49 -0
- data/lib/query_helper/version.rb +3 -0
- data/lib/query_helper.rb +173 -0
- data/query_helper.gemspec +51 -0
- metadata +221 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require "query_helper/invalid_query_error"
|
2
|
+
|
3
|
+
class QueryHelper
|
4
|
+
class SqlSort
|
5
|
+
|
6
|
+
attr_accessor :column_maps
|
7
|
+
|
8
|
+
def initialize(sort_string: "", column_maps: [])
|
9
|
+
@sort_string = sort_string
|
10
|
+
@column_maps = column_maps
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_sort_string
|
14
|
+
sql_strings = []
|
15
|
+
sorts = @sort_string.split(",")
|
16
|
+
sorts.each_with_index do |sort, index|
|
17
|
+
sort_alias = sort.split(":")[0]
|
18
|
+
direction = sort.split(":")[1]
|
19
|
+
modifier = sort.split(":")[2]
|
20
|
+
|
21
|
+
begin
|
22
|
+
sql_expression = @column_maps.find{ |m| m.alias_name == sort_alias }.sql_expression
|
23
|
+
rescue NoMethodError => e
|
24
|
+
raise InvalidQueryError.new("Sorting not allowed on column '#{sort_alias}'")
|
25
|
+
end
|
26
|
+
|
27
|
+
if direction == "desc"
|
28
|
+
case ActiveRecord::Base.connection.adapter_name
|
29
|
+
when "SQLite" # SQLite is used in the test suite
|
30
|
+
direction = "desc"
|
31
|
+
else
|
32
|
+
direction = "desc nulls last"
|
33
|
+
end
|
34
|
+
else
|
35
|
+
direction = "asc"
|
36
|
+
end
|
37
|
+
|
38
|
+
case modifier
|
39
|
+
when "lowercase"
|
40
|
+
sql_expression = "lower(#{sql_expression})"
|
41
|
+
end
|
42
|
+
|
43
|
+
sql_strings << "#{sql_expression} #{direction}"
|
44
|
+
end
|
45
|
+
|
46
|
+
return sql_strings
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/query_helper.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
require "query_helper/version"
|
4
|
+
require "query_helper/filter"
|
5
|
+
require "query_helper/column_map"
|
6
|
+
require "query_helper/associations"
|
7
|
+
require "query_helper/query_helper_concern"
|
8
|
+
require "query_helper/sql_parser"
|
9
|
+
require "query_helper/sql_manipulator"
|
10
|
+
require "query_helper/sql_filter"
|
11
|
+
require "query_helper/sql_sort"
|
12
|
+
require "query_helper/invalid_query_error"
|
13
|
+
|
14
|
+
class QueryHelper
|
15
|
+
|
16
|
+
attr_accessor :model, :query, :bind_variables, :sql_filter, :sql_sort, :page, :per_page, :single_record, :associations, :as_json_options, :executed_query, :api_payload
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
model: nil, # the model to run the query against
|
20
|
+
query: nil, # a sql string or an active record query
|
21
|
+
bind_variables: {}, # a list of bind variables to be embedded into the query
|
22
|
+
sql_filter: SqlFilter.new(), # a SqlFilter object
|
23
|
+
sql_sort: SqlSort.new(), # a SqlSort object
|
24
|
+
page: nil, # define the page you want returned
|
25
|
+
per_page: nil, # define how many results you want per page
|
26
|
+
single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned
|
27
|
+
associations: nil, # a list of activerecord associations you'd like included in the payload
|
28
|
+
as_json_options: nil, # a list of as_json options you'd like run before returning the payload
|
29
|
+
custom_mappings: {}, # custom keyword => sql_expression mappings
|
30
|
+
api_payload: false # Return the paginated payload or simply return the result array
|
31
|
+
)
|
32
|
+
@model = model
|
33
|
+
@query = query
|
34
|
+
@bind_variables = bind_variables
|
35
|
+
@sql_filter = sql_filter
|
36
|
+
@sql_sort = sql_sort
|
37
|
+
@page = page.to_i if page
|
38
|
+
@per_page = per_page.to_i if per_page
|
39
|
+
@single_record = single_record
|
40
|
+
@associations = associations
|
41
|
+
@as_json_options = as_json_options
|
42
|
+
@custom_mappings = custom_mappings
|
43
|
+
@api_payload = api_payload
|
44
|
+
|
45
|
+
if @page && @per_page
|
46
|
+
# Determine limit and offset
|
47
|
+
limit = @per_page
|
48
|
+
offset = (@page - 1) * @per_page
|
49
|
+
|
50
|
+
# Merge limit/offset variables into bind_variables
|
51
|
+
@bind_variables.merge!({limit: limit, offset: offset})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_query(query: nil, model:nil, bind_variables: {})
|
56
|
+
@model = model if model
|
57
|
+
@query = query if query
|
58
|
+
@bind_variables.merge!(bind_variables)
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_filter(operator_code:, criterion:, comparate:)
|
62
|
+
@sql_filter.filter_values["comparate"] = { operator_code => criterion }
|
63
|
+
end
|
64
|
+
|
65
|
+
def execute_query
|
66
|
+
# Correctly set the query and model based on query type
|
67
|
+
determine_query_type()
|
68
|
+
|
69
|
+
# Create column maps to be used by the filter and sort objects
|
70
|
+
column_maps = create_column_maps()
|
71
|
+
@sql_filter.column_maps = column_maps
|
72
|
+
@sql_sort.column_maps = column_maps
|
73
|
+
|
74
|
+
# create the filters from the column maps
|
75
|
+
@sql_filter.create_filters()
|
76
|
+
|
77
|
+
# merge the filter bind variables into the query bind variables
|
78
|
+
@bind_variables.merge!(@sql_filter.bind_variables)
|
79
|
+
|
80
|
+
# Execute Sql Query
|
81
|
+
manipulator = SqlManipulator.new(
|
82
|
+
sql: @query,
|
83
|
+
where_clauses: @sql_filter.where_clauses,
|
84
|
+
having_clauses: @sql_filter.having_clauses,
|
85
|
+
order_by_clauses: @sql_sort.parse_sort_string,
|
86
|
+
include_limit_clause: @page && @per_page ? true : false
|
87
|
+
)
|
88
|
+
@executed_query = manipulator.build()
|
89
|
+
@results = @model.find_by_sql([@executed_query, @bind_variables]) # Execute Sql Query
|
90
|
+
@results = @results.first if @single_record # Return a single result if requested
|
91
|
+
|
92
|
+
determine_count()
|
93
|
+
load_associations()
|
94
|
+
clean_results()
|
95
|
+
end
|
96
|
+
|
97
|
+
def results()
|
98
|
+
execute_query()
|
99
|
+
return paginated_results() if @api_payload
|
100
|
+
return @results
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def paginated_results
|
108
|
+
{ pagination: pagination_results(),
|
109
|
+
data: @results }
|
110
|
+
end
|
111
|
+
|
112
|
+
def determine_query_type
|
113
|
+
# If a custom sql string is passed in, make sure a valid model is passed in as well
|
114
|
+
if @query.class == String
|
115
|
+
raise InvalidQueryError.new("a valid model must be included to run a custom SQL query") unless @model < ActiveRecord::Base
|
116
|
+
# If an active record query is passed in, find the model and sql from the query
|
117
|
+
elsif @query.class < ActiveRecord::Relation
|
118
|
+
@model = @query.model
|
119
|
+
@query = @query.to_sql
|
120
|
+
else
|
121
|
+
raise InvalidQueryError.new("unable to determine query type")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def determine_count
|
126
|
+
# Determine total result count (unpaginated)
|
127
|
+
@count = @page && @per_page && @results.length > 0 ? @results.first["_query_full_count"] : @results.length
|
128
|
+
end
|
129
|
+
|
130
|
+
def load_associations
|
131
|
+
@results = Associations.load_associations(
|
132
|
+
payload: @results,
|
133
|
+
associations: @associations,
|
134
|
+
as_json_options: @as_json_options
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
def clean_results
|
139
|
+
@results.map!{ |r| r.except("_query_full_count") } if @page && @per_page
|
140
|
+
end
|
141
|
+
|
142
|
+
def pagination_results
|
143
|
+
# Set pagination params if they aren't provided
|
144
|
+
@per_page = @count unless @per_page
|
145
|
+
@page = 1 unless @page
|
146
|
+
|
147
|
+
total_pages = (@count/(@per_page.nonzero? || 1).to_f).ceil
|
148
|
+
next_page = @page + 1 if @page.between?(1, total_pages - 1)
|
149
|
+
previous_page = @page - 1 if @page.between?(2, total_pages)
|
150
|
+
first_page = @page == 1
|
151
|
+
last_page = @page == total_pages
|
152
|
+
out_of_range = !@page.between?(1,total_pages)
|
153
|
+
|
154
|
+
{ count: @count,
|
155
|
+
current_page: @page,
|
156
|
+
next_page: next_page,
|
157
|
+
previous_page: previous_page,
|
158
|
+
total_pages: total_pages,
|
159
|
+
per_page: @per_page,
|
160
|
+
first_page: first_page,
|
161
|
+
last_page: last_page,
|
162
|
+
out_of_range: out_of_range }
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_column_maps
|
166
|
+
ColumnMap.create_column_mappings(
|
167
|
+
query: @query,
|
168
|
+
custom_mappings: @custom_mappings,
|
169
|
+
model: @model
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "query_helper/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "query_helper"
|
8
|
+
spec.version = QueryHelper::VERSION
|
9
|
+
spec.authors = ["Evan McDaniel"]
|
10
|
+
spec.email = ["eamigo13@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby Gem to help with pagination and data formatting at Pattern, Inc.}
|
13
|
+
spec.description = %q{Ruby gem developed to help with pagination, filtering, sorting, and including associations on both active record queries and custom sql queries}
|
14
|
+
spec.homepage = "https://github.com/iserve-products/query_helper"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
+
#
|
22
|
+
# spec.metadata["homepage_uri"] = spec.homepage
|
23
|
+
# spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
24
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
25
|
+
else
|
26
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
27
|
+
"public gem pushes."
|
28
|
+
end
|
29
|
+
|
30
|
+
# Specify which files should be added to the gem when it is released.
|
31
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
32
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
34
|
+
end
|
35
|
+
spec.bindir = "exe"
|
36
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
37
|
+
spec.require_paths = ["lib"]
|
38
|
+
|
39
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
40
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
41
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
42
|
+
spec.add_development_dependency "sqlite3", "~> 1.3.6"
|
43
|
+
spec.add_development_dependency "faker", "~> 1.9.3"
|
44
|
+
spec.add_development_dependency "byebug"
|
45
|
+
spec.add_development_dependency 'rspec-rails'
|
46
|
+
spec.add_development_dependency 'actionpack'
|
47
|
+
spec.add_development_dependency 'activesupport'
|
48
|
+
|
49
|
+
spec.add_dependency "activerecord", "~> 5.0"
|
50
|
+
spec.add_dependency "activesupport", "~> 5.0"
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: query_helper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Evan McDaniel
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.6
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.3.6
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: faker
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.9.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.9.3
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: actionpack
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: activesupport
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: activerecord
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '5.0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '5.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: activesupport
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '5.0'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '5.0'
|
167
|
+
description: Ruby gem developed to help with pagination, filtering, sorting, and including
|
168
|
+
associations on both active record queries and custom sql queries
|
169
|
+
email:
|
170
|
+
- eamigo13@gmail.com
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- ".gitignore"
|
176
|
+
- ".rspec"
|
177
|
+
- ".travis.yml"
|
178
|
+
- CODE_OF_CONDUCT.md
|
179
|
+
- Gemfile
|
180
|
+
- Gemfile.lock
|
181
|
+
- LICENSE.txt
|
182
|
+
- README.md
|
183
|
+
- Rakefile
|
184
|
+
- bin/console
|
185
|
+
- bin/setup
|
186
|
+
- lib/query_helper.rb
|
187
|
+
- lib/query_helper/associations.rb
|
188
|
+
- lib/query_helper/column_map.rb
|
189
|
+
- lib/query_helper/filter.rb
|
190
|
+
- lib/query_helper/invalid_query_error.rb
|
191
|
+
- lib/query_helper/query_helper_concern.rb
|
192
|
+
- lib/query_helper/sql_filter.rb
|
193
|
+
- lib/query_helper/sql_manipulator.rb
|
194
|
+
- lib/query_helper/sql_parser.rb
|
195
|
+
- lib/query_helper/sql_sort.rb
|
196
|
+
- lib/query_helper/version.rb
|
197
|
+
- query_helper.gemspec
|
198
|
+
homepage: https://github.com/iserve-products/query_helper
|
199
|
+
licenses:
|
200
|
+
- MIT
|
201
|
+
metadata: {}
|
202
|
+
post_install_message:
|
203
|
+
rdoc_options: []
|
204
|
+
require_paths:
|
205
|
+
- lib
|
206
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - ">="
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '0'
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
requirements: []
|
217
|
+
rubygems_version: 3.0.3
|
218
|
+
signing_key:
|
219
|
+
specification_version: 4
|
220
|
+
summary: Ruby Gem to help with pagination and data formatting at Pattern, Inc.
|
221
|
+
test_files: []
|