dolly 1.1.7 → 3.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 +4 -4
- data/README.md +35 -0
- data/lib/dolly.rb +1 -23
- data/lib/dolly/attachment.rb +29 -0
- data/lib/dolly/bulk_document.rb +27 -26
- data/lib/dolly/class_methods_delegation.rb +15 -0
- data/lib/dolly/collection.rb +26 -69
- data/lib/dolly/configuration.rb +35 -10
- data/lib/dolly/connection.rb +91 -22
- data/lib/dolly/depracated_database.rb +24 -0
- data/lib/dolly/document.rb +32 -206
- data/lib/dolly/document_creation.rb +20 -0
- data/lib/dolly/document_state.rb +65 -0
- data/lib/dolly/document_type.rb +28 -0
- data/lib/dolly/exceptions.rb +21 -0
- data/lib/dolly/identity_properties.rb +29 -0
- data/lib/dolly/properties.rb +31 -0
- data/lib/dolly/property.rb +58 -47
- data/lib/dolly/property_manager.rb +47 -0
- data/lib/dolly/property_set.rb +18 -0
- data/lib/dolly/query.rb +39 -67
- data/lib/dolly/query_arguments.rb +35 -0
- data/lib/dolly/request.rb +12 -107
- data/lib/dolly/request_header.rb +26 -0
- data/lib/dolly/timestamp.rb +24 -0
- data/lib/dolly/version.rb +1 -1
- data/lib/{dolly → railties}/railtie.rb +2 -1
- data/lib/refinements/string_refinements.rb +28 -0
- data/lib/tasks/db.rake +4 -3
- data/test/bulk_document_test.rb +8 -5
- data/test/document_test.rb +137 -53
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/log/test.log +46417 -46858
- data/test/test_helper.rb +14 -20
- metadata +42 -145
- data/Rakefile +0 -11
- data/lib/dolly/bulk_error.rb +0 -16
- data/lib/dolly/db_config.rb +0 -20
- data/lib/dolly/interpreter.rb +0 -5
- data/lib/dolly/logger.rb +0 -9
- data/lib/dolly/name_space.rb +0 -28
- data/lib/dolly/timestamps.rb +0 -21
- data/lib/exceptions/dolly.rb +0 -47
- data/test/collection_test.rb +0 -59
- data/test/configuration_test.rb +0 -9
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -13
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -27
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/couchdb.yml +0 -13
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -29
- data/test/dummy/config/environments/production.rb +0 -80
- data/test/dummy/config/environments/test.rb +0 -36
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -12
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/lib/couch_rest_adapter/railtie.rb +0 -10
- data/test/dummy/public/404.html +0 -58
- data/test/dummy/public/422.html +0 -58
- data/test/dummy/public/500.html +0 -57
- data/test/dummy/public/favicon.ico +0 -0
- data/test/factories/factories.rb +0 -8
- data/test/request_test.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c26d50611f8efd7f5cd7b973256ddb6d885a747a
|
4
|
+
data.tar.gz: 4c16a1b2b21b55eeba0a9b7c0307dce20af3be90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4a534952347905e024d8da5eec35a31220305edb10528e2d8e30608691ab42143a09c08a005f0121c6acda6a7f6c53320907eb9c81f4b821966d1980c6040d8
|
7
|
+
data.tar.gz: b4f1d1f5994aa3d963d2fe77f2c8e4b67e4d657c66a4da5548924630da64d28a70f0902b9bd503c08c4a982a47a34e188a0468ee2559eab8a0c1599ede6881c1
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Dolly3
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dolly3`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'dolly3'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install dolly3
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dolly3.
|
data/lib/dolly.rb
CHANGED
@@ -1,28 +1,6 @@
|
|
1
1
|
require "dolly/version"
|
2
2
|
require "dolly/document"
|
3
|
-
require
|
4
|
-
require 'dolly/railtie' if defined?(Rails)
|
3
|
+
require 'railties/railtie' if defined?(Rails)
|
5
4
|
|
6
5
|
module Dolly
|
7
|
-
class << self
|
8
|
-
def configure
|
9
|
-
yield config
|
10
|
-
end
|
11
|
-
|
12
|
-
def config
|
13
|
-
@config ||= Configuration.new
|
14
|
-
end
|
15
|
-
|
16
|
-
def reset!
|
17
|
-
@config = Configuration.new
|
18
|
-
end
|
19
|
-
|
20
|
-
def log_requests?
|
21
|
-
!!config.log_requests
|
22
|
-
end
|
23
|
-
|
24
|
-
def logger
|
25
|
-
@logger ||= config.logger
|
26
|
-
end
|
27
|
-
end
|
28
6
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Dolly
|
4
|
+
module Attachment
|
5
|
+
def attach_file! file_name, mime_type, body, opts={}
|
6
|
+
attach_file file_name, mime_type, body, opts
|
7
|
+
save
|
8
|
+
end
|
9
|
+
|
10
|
+
def attach_file file_name, mime_type, body, opts={}
|
11
|
+
if opts[:inline]
|
12
|
+
attach_inline_file file_name, mime_type, body
|
13
|
+
else
|
14
|
+
attach_standalone_file file_name, mime_type, body
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def attach_inline_file file_name, mime_type, body
|
19
|
+
attachment_data = { file_name.to_s => { 'content_type' => mime_type,
|
20
|
+
'data' => Base64.encode64(body)} }
|
21
|
+
doc['_attachments'] ||= {}
|
22
|
+
doc['_attachments'].merge! attachment_data
|
23
|
+
end
|
24
|
+
|
25
|
+
def attach_standalone_file file_name, mime_type, body
|
26
|
+
self.class.connection.attach id_as_resource, CGI.escape(file_name), body, { 'Content-Type' => mime_type }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/dolly/bulk_document.rb
CHANGED
@@ -1,37 +1,34 @@
|
|
1
|
-
require 'dolly/bulk_error'
|
2
|
-
|
3
1
|
module Dolly
|
4
2
|
class BulkDocument
|
5
|
-
include Enumerable
|
6
3
|
extend Forwardable
|
7
4
|
|
8
|
-
DOC_NAME
|
5
|
+
DOC_NAME = '_bulk_docs'
|
9
6
|
|
10
|
-
attr_reader :payload, :
|
7
|
+
attr_reader :payload, :connection
|
11
8
|
attr_accessor :errors, :response
|
12
9
|
|
13
10
|
def_delegators :docs, :[], :<<
|
14
11
|
|
15
|
-
def initialize
|
16
|
-
@
|
12
|
+
def initialize(connection, ary = [])
|
13
|
+
@connection = connection
|
17
14
|
@payload = Hash.new
|
18
|
-
|
15
|
+
payload[:docs] = ary
|
19
16
|
end
|
20
17
|
|
21
18
|
def docs
|
22
|
-
|
19
|
+
payload[:docs]
|
23
20
|
end
|
24
21
|
|
25
22
|
def save
|
26
23
|
return if docs.empty?
|
27
|
-
self.response =
|
24
|
+
self.response = connection.post(DOC_NAME, docs_payload)
|
28
25
|
build_errors
|
29
26
|
update_revs
|
30
27
|
end
|
31
28
|
|
32
29
|
def delete
|
33
30
|
return if docs.empty?
|
34
|
-
|
31
|
+
connection.post DOC_NAME, json_payload(_deleted: true)
|
35
32
|
end
|
36
33
|
|
37
34
|
def clear
|
@@ -45,41 +42,45 @@ module Dolly
|
|
45
42
|
end
|
46
43
|
|
47
44
|
private
|
45
|
+
|
48
46
|
def update_revs
|
49
|
-
|
50
|
-
next if doc[
|
51
|
-
item =
|
47
|
+
response.each do |doc|
|
48
|
+
next if doc[:error]
|
49
|
+
item = payload[:docs].detect { |d| d.id == doc[:id] }
|
50
|
+
|
52
51
|
if item.nil?
|
53
|
-
|
52
|
+
errors << response_error(item)
|
54
53
|
next
|
55
54
|
end
|
56
|
-
|
57
|
-
|
55
|
+
|
56
|
+
item.rev = doc[:rev]
|
57
|
+
payload[:docs].delete(item)
|
58
58
|
end
|
59
|
+
|
59
60
|
clean_response
|
60
61
|
end
|
61
62
|
|
62
63
|
def clean_response
|
63
|
-
|
64
|
+
response.delete_if { |d| !d[:error] }
|
64
65
|
end
|
65
66
|
|
66
67
|
def build_errors
|
67
68
|
self.errors = response_errors.map do |err|
|
68
|
-
obj =
|
69
|
-
BulkError.new err.merge!(
|
69
|
+
obj = payload[:docs].detect { |d| d.id == err[:id]} if err[:id]
|
70
|
+
BulkError.new err.merge!(obj: obj)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
def
|
74
|
-
|
74
|
+
def docs_payload opts = {}
|
75
|
+
{ docs: docs.map {|d| d.to_h.merge(opts) } }
|
75
76
|
end
|
76
77
|
|
77
|
-
def
|
78
|
-
{
|
78
|
+
def response_errors
|
79
|
+
self.response.select{ |d| d[:error] }
|
79
80
|
end
|
80
81
|
|
81
|
-
def
|
82
|
-
|
82
|
+
def response_error(item)
|
83
|
+
BulkError.new(error: 'Document saved but not local rev updated.', reason: "Document with id #{doc['id']} on bulk doc was not found in payload.", obj: nil)
|
83
84
|
end
|
84
85
|
end
|
85
86
|
end
|
data/lib/dolly/collection.rb
CHANGED
@@ -1,88 +1,45 @@
|
|
1
1
|
module Dolly
|
2
|
-
class Collection < DelegateClass(
|
3
|
-
|
4
|
-
attr_writer :json, :docs_class
|
2
|
+
class Collection < DelegateClass(Array)
|
3
|
+
attr_reader :info
|
5
4
|
|
6
|
-
def initialize
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
super(
|
11
|
-
load
|
5
|
+
def initialize(rows: [], **info)
|
6
|
+
@info = info
|
7
|
+
#TODO: We should raise an exception if one of the
|
8
|
+
# requested documents is missing
|
9
|
+
super rows.map(&collect_docs).compact
|
12
10
|
end
|
13
11
|
|
14
|
-
def
|
15
|
-
|
12
|
+
def first_or_all(forced_first = false)
|
13
|
+
return self if forced_first
|
14
|
+
single? ? first : self
|
16
15
|
end
|
17
16
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
regex = %r{
|
22
|
-
\"#{key}\": # find key definition in json string
|
23
|
-
( # start value group
|
24
|
-
\"[^\"]*\" # find anything (even empty) between \" and \"
|
25
|
-
| # logical OR
|
26
|
-
null #literal null value
|
27
|
-
) # end value group
|
28
|
-
}x
|
29
|
-
|
30
|
-
raise Dolly::MissingPropertyError unless json.match regex
|
31
|
-
json.gsub! regex, "\"#{key}\":\"#{value}\""
|
32
|
-
end
|
33
|
-
|
34
|
-
BulkDocument.new(Dolly::Document.database, to_a).save
|
35
|
-
clear
|
36
|
-
load
|
37
|
-
self
|
17
|
+
def single?
|
18
|
+
size <= 1
|
38
19
|
end
|
39
20
|
|
40
|
-
|
41
|
-
load if empty?
|
42
|
-
super &block
|
43
|
-
#TODO: returning nil to avoid extra time serializing set.
|
44
|
-
nil
|
45
|
-
end
|
21
|
+
private
|
46
22
|
|
47
|
-
def
|
48
|
-
|
49
|
-
next unless
|
50
|
-
|
51
|
-
|
52
|
-
rev = properties.delete '_rev' if properties['_rev']
|
53
|
-
document = (docs_class || doc_class(id)).new properties
|
54
|
-
document.doc = properties.merge({'_id' => id, '_rev' => rev})
|
55
|
-
self << document
|
23
|
+
def collect_docs
|
24
|
+
lambda do |row|
|
25
|
+
next unless collectable_row?(row)
|
26
|
+
klass = Object.const_get doc_type(row[:id])
|
27
|
+
klass.from_doc(row[:doc])
|
56
28
|
end
|
57
29
|
end
|
58
30
|
|
59
|
-
def
|
60
|
-
|
61
|
-
self.rows = parsed['rows']
|
31
|
+
def doc_type(key)
|
32
|
+
key.match(%r{^([^/]+)/})[1].split('_').collect(&:capitalize).join
|
62
33
|
end
|
63
34
|
|
64
|
-
def
|
65
|
-
|
66
|
-
map{|r| r.doc }.to_json(options)
|
35
|
+
def collectable_row?(row)
|
36
|
+
!deleted_doc?(row) && row[:error].nil?
|
67
37
|
end
|
68
38
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def doc_class id
|
75
|
-
# TODO: We need to improve and document the way we return
|
76
|
-
# multiple types when querying from a class, as it might
|
77
|
-
# be confusing. We *could* also get dolly to parse the result
|
78
|
-
# before sending it back to the client.
|
79
|
-
doc_class = id[/^[a-z_]+/].camelize.constantize
|
80
|
-
docs_class == doc_class ? docs_class : doc_class
|
39
|
+
def deleted_doc?(row)
|
40
|
+
value = row&.fetch(:value, {})
|
41
|
+
return false unless value.is_a? Hash
|
42
|
+
value.fetch(:deleted, false)
|
81
43
|
end
|
82
|
-
|
83
|
-
def json
|
84
|
-
@json
|
85
|
-
end
|
86
|
-
|
87
44
|
end
|
88
45
|
end
|
data/lib/dolly/configuration.rb
CHANGED
@@ -1,18 +1,43 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'erb'
|
2
3
|
|
3
4
|
module Dolly
|
4
|
-
|
5
|
-
|
5
|
+
module Configuration
|
6
|
+
attr_writer :config_file
|
6
7
|
|
7
|
-
def
|
8
|
-
@
|
9
|
-
@log_path = $stdout
|
10
|
-
@log = :dolly
|
8
|
+
def env
|
9
|
+
@env ||= configuration[db.to_s]
|
11
10
|
end
|
12
11
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
12
|
+
def base_uri
|
13
|
+
"#{protocol}#{host}#{port}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def protocol
|
17
|
+
"#{env['protocol']}://"
|
18
|
+
end
|
19
|
+
|
20
|
+
def host
|
21
|
+
env['host']
|
22
|
+
end
|
23
|
+
|
24
|
+
def port
|
25
|
+
return unless env['port']
|
26
|
+
":#{env['port']}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def db_name
|
30
|
+
env['name']
|
31
|
+
end
|
32
|
+
|
33
|
+
def configuration
|
34
|
+
@config_data ||= File.read(config_file)
|
35
|
+
raise Dolly::InvalidConfigFileError if @config_data&.empty?
|
36
|
+
YAML::load(ERB.new(@config_data).result)[app_env.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_file
|
40
|
+
@config_file ||= File.join('config', 'couchdb.yml')
|
16
41
|
end
|
17
42
|
end
|
18
43
|
end
|
data/lib/dolly/connection.rb
CHANGED
@@ -1,42 +1,111 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'oj'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/http'
|
4
|
+
require 'dolly/request_header'
|
5
|
+
require 'dolly/exceptions'
|
6
|
+
require 'dolly/configuration'
|
7
|
+
require 'refinements/string_refinements'
|
5
8
|
|
6
9
|
module Dolly
|
7
|
-
|
8
|
-
include Dolly::
|
9
|
-
|
10
|
+
class Connection
|
11
|
+
include Dolly::Configuration
|
12
|
+
attr_reader :db, :app_env
|
10
13
|
|
11
|
-
|
14
|
+
DEFAULT_HEADER = { 'Content-Type' => 'application/json' }
|
12
15
|
|
13
|
-
|
14
|
-
|
16
|
+
using StringRefinements
|
17
|
+
|
18
|
+
def initialize db = :default, app_env = :development
|
19
|
+
@db = db
|
20
|
+
@app_env = app_env
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(resource, data = {})
|
24
|
+
query = { query: values_to_json(data) } if data
|
25
|
+
request :get, resource.cgi_escape, query
|
26
|
+
end
|
27
|
+
|
28
|
+
def post resource, data
|
29
|
+
request :post, resource.cgi_escape, data
|
30
|
+
end
|
31
|
+
|
32
|
+
def put resource, data
|
33
|
+
request :put, resource.cgi_escape, data
|
15
34
|
end
|
16
35
|
|
17
|
-
def
|
18
|
-
|
36
|
+
def delete resource, rev
|
37
|
+
request :delete, resource.cgi_escape, query: { rev: rev }
|
19
38
|
end
|
20
39
|
|
21
|
-
def
|
22
|
-
|
40
|
+
def view resource, opts
|
41
|
+
request :get, resource, query: values_to_json({include_docs: true}.merge!(opts))
|
23
42
|
end
|
24
43
|
|
25
|
-
def
|
26
|
-
|
44
|
+
def attach resource, attachment_name, data, headers = {}
|
45
|
+
request :put, "#{resource.cgi_escape}/#{attachment_name}", { _body: data }.merge(headers: headers)
|
27
46
|
end
|
28
47
|
|
29
|
-
def
|
30
|
-
"
|
48
|
+
def uuids opts = {}
|
49
|
+
tools("_uuids", opts)[:uuids]
|
31
50
|
end
|
32
51
|
|
33
|
-
def
|
34
|
-
"
|
52
|
+
def stats
|
53
|
+
get("/#{db_name}")
|
35
54
|
end
|
36
55
|
|
37
|
-
def
|
38
|
-
|
56
|
+
def tools path, opts = nil
|
57
|
+
request(:get, "/#{path}", opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
def request(method, resource, data = {})
|
61
|
+
headers = Dolly::HeaderRequest.new data.delete(:headers)
|
62
|
+
uri = build_uri(resource, data.delete(:query))
|
63
|
+
klass = request_method(method)
|
64
|
+
req = klass.new(uri, headers)
|
65
|
+
req.body = format_data(data, headers.json?)
|
66
|
+
response = start_request(req)
|
67
|
+
|
68
|
+
response_format(response)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def start_request(req)
|
74
|
+
Net::HTTP.start(req.uri.hostname, req.uri.port) do |http|
|
75
|
+
req.basic_auth env['username'], env['password'] if env['username'].present?
|
76
|
+
http.request(req)
|
77
|
+
end
|
39
78
|
end
|
40
79
|
|
80
|
+
def response_format(res)
|
81
|
+
raise Dolly::ResourceNotFound if res.code.to_i == 404
|
82
|
+
raise Dolly::ServerError.new(res.body) if (400..600).include? res.code.to_i
|
83
|
+
Oj.load(res.body, symbol_keys: true)
|
84
|
+
end
|
85
|
+
|
86
|
+
def format_data(data = nil, is_json)
|
87
|
+
return unless data
|
88
|
+
body = data.delete(:_body) || data
|
89
|
+
is_json ? body.to_json : body
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_uri(resource, query = nil)
|
93
|
+
query_str = "?#{to_query(query)}" if query
|
94
|
+
uri = (resource =~ %r{^/}) ? resource : "/#{db_name}/#{resource}"
|
95
|
+
|
96
|
+
URI("#{base_uri}#{uri}#{query_str}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def request_method(method_name)
|
100
|
+
Object.const_get("Net::HTTP::#{method_name.capitalize}")
|
101
|
+
end
|
102
|
+
|
103
|
+
def values_to_json hash
|
104
|
+
hash.each_with_object({}) { |(k,v), h| h[k] = v.is_a?(Numeric) ? v : v.to_json }
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_query(string)
|
108
|
+
string.map { |k, v| "#{k}=#{v}" }.sort * '&'
|
109
|
+
end
|
41
110
|
end
|
42
111
|
end
|