pliny 0.0.1.pre
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/bin/pliny-generate +6 -0
- data/lib/pliny.rb +21 -0
- data/lib/pliny/commands/generator.rb +197 -0
- data/lib/pliny/config_helpers.rb +24 -0
- data/lib/pliny/errors.rb +109 -0
- data/lib/pliny/extensions/instruments.rb +38 -0
- data/lib/pliny/log.rb +84 -0
- data/lib/pliny/middleware/cors.rb +46 -0
- data/lib/pliny/middleware/request_id.rb +42 -0
- data/lib/pliny/middleware/request_store.rb +14 -0
- data/lib/pliny/middleware/rescue_errors.rb +24 -0
- data/lib/pliny/middleware/timeout.rb +25 -0
- data/lib/pliny/middleware/versioning.rb +64 -0
- data/lib/pliny/request_store.rb +22 -0
- data/lib/pliny/router.rb +15 -0
- data/lib/pliny/tasks.rb +3 -0
- data/lib/pliny/tasks/db.rake +116 -0
- data/lib/pliny/tasks/test.rake +8 -0
- data/lib/pliny/templates/endpoint.erb +30 -0
- data/lib/pliny/templates/endpoint_acceptance_test.erb +40 -0
- data/lib/pliny/templates/endpoint_scaffold.erb +49 -0
- data/lib/pliny/templates/endpoint_scaffold_acceptance_test.erb +55 -0
- data/lib/pliny/templates/endpoint_test.erb +16 -0
- data/lib/pliny/templates/mediator.erb +22 -0
- data/lib/pliny/templates/mediator_test.erb +5 -0
- data/lib/pliny/templates/migration.erb +9 -0
- data/lib/pliny/templates/model.erb +5 -0
- data/lib/pliny/templates/model_migration.erb +10 -0
- data/lib/pliny/templates/model_test.erb +5 -0
- data/lib/pliny/utils.rb +31 -0
- data/lib/pliny/version.rb +3 -0
- data/test/commands/generator_test.rb +147 -0
- data/test/errors_test.rb +24 -0
- data/test/extensions/instruments_test.rb +34 -0
- data/test/log_test.rb +27 -0
- data/test/middleware/cors_test.rb +42 -0
- data/test/middleware/request_id_test.rb +28 -0
- data/test/middleware/request_store_test.rb +25 -0
- data/test/middleware/rescue_errors_test.rb +41 -0
- data/test/middleware/timeout_test.rb +32 -0
- data/test/middleware/versioning_test.rb +63 -0
- data/test/request_store_test.rb +25 -0
- data/test/router_test.rb +39 -0
- data/test/test_helper.rb +18 -0
- metadata +252 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 18bf67cc7da9438b4f46b8a0265edbfb4253484f
|
4
|
+
data.tar.gz: 09a5892f943e2aedea7fda831a85924212902deb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b415fc1bb35256ee85dd80bde4bf4fcdfb1995839646c59cfb1cc162bed23b76cbbc0e1cf7bb9155b85971156747c4b3c238a3b900d8f8ccc923c1b1d46a272d
|
7
|
+
data.tar.gz: 55aa879eec76133b9c01e979a92a332e99ea1a7af61ec3eab861488a3187aee134b7875271d9ddb91248618a060dac70afcce77847ab7688256e33decea4263d
|
data/bin/pliny-generate
ADDED
data/lib/pliny.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "sinatra/base"
|
3
|
+
|
4
|
+
require "pliny/version"
|
5
|
+
require "pliny/commands/generator"
|
6
|
+
require "pliny/errors"
|
7
|
+
require "pliny/extensions/instruments"
|
8
|
+
require "pliny/log"
|
9
|
+
require "pliny/request_store"
|
10
|
+
require "pliny/router"
|
11
|
+
require "pliny/utils"
|
12
|
+
require "pliny/middleware/cors"
|
13
|
+
require "pliny/middleware/request_id"
|
14
|
+
require "pliny/middleware/request_store"
|
15
|
+
require "pliny/middleware/rescue_errors"
|
16
|
+
require "pliny/middleware/timeout"
|
17
|
+
require "pliny/middleware/versioning"
|
18
|
+
|
19
|
+
module Pliny
|
20
|
+
extend Log
|
21
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "fileutils"
|
3
|
+
require "ostruct"
|
4
|
+
require "active_support/inflector"
|
5
|
+
require "prmd"
|
6
|
+
|
7
|
+
module Pliny::Commands
|
8
|
+
class Generator
|
9
|
+
attr_accessor :args, :stream
|
10
|
+
|
11
|
+
def self.run(args, stream=$stdout)
|
12
|
+
new(args).run!
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(args={}, stream=$stdout)
|
16
|
+
@args = args
|
17
|
+
@stream = stream
|
18
|
+
end
|
19
|
+
|
20
|
+
def run!
|
21
|
+
unless type
|
22
|
+
raise "Missing type of object to generate"
|
23
|
+
end
|
24
|
+
unless name
|
25
|
+
raise "Missing #{type} name"
|
26
|
+
end
|
27
|
+
|
28
|
+
case type
|
29
|
+
when "endpoint"
|
30
|
+
create_endpoint(scaffold: false)
|
31
|
+
create_endpoint_test
|
32
|
+
create_endpoint_acceptance_test(scaffold: false)
|
33
|
+
when "mediator"
|
34
|
+
create_mediator
|
35
|
+
create_mediator_test
|
36
|
+
when "migration"
|
37
|
+
create_migration
|
38
|
+
when "model"
|
39
|
+
create_model
|
40
|
+
create_model_migration
|
41
|
+
create_model_test
|
42
|
+
when "scaffold"
|
43
|
+
create_endpoint(scaffold: true)
|
44
|
+
create_endpoint_test
|
45
|
+
create_endpoint_acceptance_test(scaffold: true)
|
46
|
+
create_model
|
47
|
+
create_model_migration
|
48
|
+
create_model_test
|
49
|
+
create_schema
|
50
|
+
rebuild_schema
|
51
|
+
when "schema"
|
52
|
+
create_schema
|
53
|
+
rebuild_schema
|
54
|
+
else
|
55
|
+
abort("Don't know how to generate '#{type}'.")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def type
|
60
|
+
args.first
|
61
|
+
end
|
62
|
+
|
63
|
+
def name
|
64
|
+
args[1]
|
65
|
+
end
|
66
|
+
|
67
|
+
def singular_class_name
|
68
|
+
name.singularize.camelize
|
69
|
+
end
|
70
|
+
|
71
|
+
def plural_class_name
|
72
|
+
name.pluralize.camelize
|
73
|
+
end
|
74
|
+
|
75
|
+
def field_name
|
76
|
+
name.tableize.singularize
|
77
|
+
end
|
78
|
+
|
79
|
+
def table_name
|
80
|
+
name.tableize
|
81
|
+
end
|
82
|
+
|
83
|
+
def display(msg)
|
84
|
+
stream.puts msg
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_endpoint(options = {})
|
88
|
+
endpoint = "./lib/endpoints/#{name.pluralize}.rb"
|
89
|
+
template = options[:scaffold] ? "endpoint_scaffold.erb" : "endpoint.erb"
|
90
|
+
render_template(template, endpoint, {
|
91
|
+
plural_class_name: plural_class_name,
|
92
|
+
singular_class_name: singular_class_name,
|
93
|
+
field_name: field_name,
|
94
|
+
url_path: url_path,
|
95
|
+
})
|
96
|
+
display "created endpoint file #{endpoint}"
|
97
|
+
display "add the following to lib/routes.rb:"
|
98
|
+
display " mount Endpoints::#{plural_class_name}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_endpoint_test
|
102
|
+
test = "./spec/endpoints/#{name.pluralize}_spec.rb"
|
103
|
+
render_template("endpoint_test.erb", test, {
|
104
|
+
plural_class_name: plural_class_name,
|
105
|
+
singular_class_name: singular_class_name,
|
106
|
+
url_path: url_path,
|
107
|
+
})
|
108
|
+
display "created test #{test}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_endpoint_acceptance_test(options = {})
|
112
|
+
test = "./spec/acceptance/#{name.pluralize}_spec.rb"
|
113
|
+
template = options[:scaffold] ? "endpoint_scaffold_acceptance_test.erb" :
|
114
|
+
"endpoint_acceptance_test.erb"
|
115
|
+
render_template(template, test, {
|
116
|
+
plural_class_name: plural_class_name,
|
117
|
+
field_name: field_name,
|
118
|
+
singular_class_name: singular_class_name,
|
119
|
+
url_path: url_path,
|
120
|
+
})
|
121
|
+
display "created test #{test}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_mediator
|
125
|
+
mediator = "./lib/mediators/#{name}.rb"
|
126
|
+
render_template("mediator.erb", mediator, plural_class_name: plural_class_name)
|
127
|
+
display "created mediator file #{mediator}"
|
128
|
+
end
|
129
|
+
|
130
|
+
def create_mediator_test
|
131
|
+
test = "./spec/mediators/#{name}_spec.rb"
|
132
|
+
render_template("mediator_test.erb", test, plural_class_name: plural_class_name)
|
133
|
+
display "created test #{test}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_migration
|
137
|
+
migration = "./db/migrate/#{Time.now.to_i}_#{name}.rb"
|
138
|
+
render_template("migration.erb", migration)
|
139
|
+
display "created migration #{migration}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_model
|
143
|
+
model = "./lib/models/#{name}.rb"
|
144
|
+
render_template("model.erb", model, singular_class_name: singular_class_name)
|
145
|
+
display "created model file #{model}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_model_migration
|
149
|
+
migration = "./db/migrate/#{Time.now.to_i}_create_#{table_name}.rb"
|
150
|
+
render_template("model_migration.erb", migration,
|
151
|
+
table_name: table_name)
|
152
|
+
display "created migration #{migration}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def create_model_test
|
156
|
+
test = "./spec/models/#{name}_spec.rb"
|
157
|
+
render_template("model_test.erb", test, singular_class_name: singular_class_name)
|
158
|
+
display "created test #{test}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_schema
|
162
|
+
schema = "./docs/schema/schemata/#{name.singularize}.yaml"
|
163
|
+
write_file(schema) do
|
164
|
+
Prmd.init(name.singularize, yaml: true)
|
165
|
+
end
|
166
|
+
display "created schema file #{schema}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def rebuild_schema
|
170
|
+
schemata = "./docs/schema.json"
|
171
|
+
write_file(schemata) do
|
172
|
+
Prmd.combine("./docs/schema/schemata", meta: "./docs/schema/meta.json")
|
173
|
+
end
|
174
|
+
display "rebuilt #{schemata}"
|
175
|
+
end
|
176
|
+
|
177
|
+
def render_template(template_file, destination_path, vars={})
|
178
|
+
template_path = File.dirname(__FILE__) + "/../templates/#{template_file}"
|
179
|
+
template = ERB.new(File.read(template_path), 0, ">")
|
180
|
+
context = OpenStruct.new(vars)
|
181
|
+
write_file(destination_path) do
|
182
|
+
template.result(context.instance_eval { binding })
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def url_path
|
187
|
+
"/" + name.pluralize.gsub(/_/, '-')
|
188
|
+
end
|
189
|
+
|
190
|
+
def write_file(destination_path)
|
191
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
192
|
+
File.open(destination_path, "w") do |f|
|
193
|
+
f.puts yield
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pliny
|
2
|
+
module ConfigHelpers
|
3
|
+
def optional(*attrs)
|
4
|
+
attrs.each do |attr|
|
5
|
+
instance_eval "def #{attr}; @#{attr} ||= ENV['#{attr.upcase}'] end", __FILE__, __LINE__
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def mandatory(*attrs)
|
10
|
+
attrs.each do |attr|
|
11
|
+
instance_eval "def #{attr}; @#{attr} ||= ENV['#{attr.upcase}'] || raise('missing=#{attr.upcase}') end", __FILE__, __LINE__
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def override(attrs)
|
16
|
+
attrs.each do |attr, value|
|
17
|
+
instance_eval "def #{attr}; @#{attr} ||= ENV['#{attr.upcase}'] || '#{value}'.to_s end", __FILE__, __LINE__
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Supress the "use RbConfig instead" warning.
|
24
|
+
Object.send :remove_const, :Config
|
data/lib/pliny/errors.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module Pliny
|
2
|
+
module Errors
|
3
|
+
class Error < StandardError
|
4
|
+
attr_accessor :id
|
5
|
+
|
6
|
+
def initialize(message, id)
|
7
|
+
@id = id
|
8
|
+
super(message)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class HTTPStatusError < Error
|
13
|
+
attr_reader :status
|
14
|
+
|
15
|
+
def initialize(message = nil, id = nil, status = nil)
|
16
|
+
meta = Pliny::Errors::META[self.class]
|
17
|
+
message = message || meta[1] + "."
|
18
|
+
id = id || meta[1].downcase.gsub(/ /, '_').to_sym
|
19
|
+
@status = status || meta[0]
|
20
|
+
super(message, id)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Continue < HTTPStatusError; end # 100
|
25
|
+
class SwitchingProtocols < HTTPStatusError; end # 101
|
26
|
+
class OK < HTTPStatusError; end # 200
|
27
|
+
class Created < HTTPStatusError; end # 201
|
28
|
+
class Accepted < HTTPStatusError; end # 202
|
29
|
+
class NonAuthoritativeInformation < HTTPStatusError; end # 203
|
30
|
+
class NoContent < HTTPStatusError; end # 204
|
31
|
+
class ResetContent < HTTPStatusError; end # 205
|
32
|
+
class PartialContent < HTTPStatusError; end # 206
|
33
|
+
class MultipleChoices < HTTPStatusError; end # 300
|
34
|
+
class MovedPermanently < HTTPStatusError; end # 301
|
35
|
+
class Found < HTTPStatusError; end # 302
|
36
|
+
class SeeOther < HTTPStatusError; end # 303
|
37
|
+
class NotModified < HTTPStatusError; end # 304
|
38
|
+
class UseProxy < HTTPStatusError; end # 305
|
39
|
+
class TemporaryRedirect < HTTPStatusError; end # 307
|
40
|
+
class BadRequest < HTTPStatusError; end # 400
|
41
|
+
class Unauthorized < HTTPStatusError; end # 401
|
42
|
+
class PaymentRequired < HTTPStatusError; end # 402
|
43
|
+
class Forbidden < HTTPStatusError; end # 403
|
44
|
+
class NotFound < HTTPStatusError; end # 404
|
45
|
+
class MethodNotAllowed < HTTPStatusError; end # 405
|
46
|
+
class NotAcceptable < HTTPStatusError; end # 406
|
47
|
+
class ProxyAuthenticationRequired < HTTPStatusError; end # 407
|
48
|
+
class RequestTimeout < HTTPStatusError; end # 408
|
49
|
+
class Conflict < HTTPStatusError; end # 409
|
50
|
+
class Gone < HTTPStatusError; end # 410
|
51
|
+
class LengthRequired < HTTPStatusError; end # 411
|
52
|
+
class PreconditionFailed < HTTPStatusError; end # 412
|
53
|
+
class RequestEntityTooLarge < HTTPStatusError; end # 413
|
54
|
+
class RequestURITooLong < HTTPStatusError; end # 414
|
55
|
+
class UnsupportedMediaType < HTTPStatusError; end # 415
|
56
|
+
class RequestedRangeNotSatisfiable < HTTPStatusError; end # 416
|
57
|
+
class ExpectationFailed < HTTPStatusError; end # 417
|
58
|
+
class UnprocessableEntity < HTTPStatusError; end # 422
|
59
|
+
class InternalServerError < HTTPStatusError; end # 500
|
60
|
+
class NotImplemented < HTTPStatusError; end # 501
|
61
|
+
class BadGateway < HTTPStatusError; end # 502
|
62
|
+
class ServiceUnavailable < HTTPStatusError; end # 503
|
63
|
+
class GatewayTimeout < HTTPStatusError; end # 504
|
64
|
+
|
65
|
+
# Messages for nicer exceptions, from rfc2616
|
66
|
+
META = {
|
67
|
+
Continue => [100, 'Continue'],
|
68
|
+
SwitchingProtocols => [101, 'Switching protocols'],
|
69
|
+
OK => [200, 'OK'],
|
70
|
+
Created => [201, 'Created'],
|
71
|
+
Accepted => [202, 'Accepted'],
|
72
|
+
NonAuthoritativeInformation => [203, 'Non-authoritative information'],
|
73
|
+
NoContent => [204, 'No content'],
|
74
|
+
ResetContent => [205, 'Reset content'],
|
75
|
+
PartialContent => [206, 'Partial content'],
|
76
|
+
MultipleChoices => [300, 'Multiple choices'],
|
77
|
+
MovedPermanently => [301, 'Moved permanently'],
|
78
|
+
Found => [302, 'Found'],
|
79
|
+
SeeOther => [303, 'See other'],
|
80
|
+
NotModified => [304, 'Not modified'],
|
81
|
+
UseProxy => [305, 'Use proxy'],
|
82
|
+
TemporaryRedirect => [307, 'Temporary redirect'],
|
83
|
+
BadRequest => [400, 'Bad request'],
|
84
|
+
Unauthorized => [401, 'Unauthorized'],
|
85
|
+
PaymentRequired => [402, 'Payment required'],
|
86
|
+
Forbidden => [403, 'Forbidden'],
|
87
|
+
NotFound => [404, 'Not found'],
|
88
|
+
MethodNotAllowed => [405, 'Method not allowed'],
|
89
|
+
NotAcceptable => [406, 'Not acceptable'],
|
90
|
+
ProxyAuthenticationRequired => [407, 'Proxy authentication required'],
|
91
|
+
RequestTimeout => [408, 'Request timeout'],
|
92
|
+
Conflict => [409, 'Conflict'],
|
93
|
+
Gone => [410, 'Gone'],
|
94
|
+
LengthRequired => [411, 'Length required'],
|
95
|
+
PreconditionFailed => [412, 'Precondition failed'],
|
96
|
+
RequestEntityTooLarge => [413, 'Request entity too large'],
|
97
|
+
RequestURITooLong => [414, 'Request-URI too long'],
|
98
|
+
UnsupportedMediaType => [415, 'Unsupported media type'],
|
99
|
+
RequestedRangeNotSatisfiable => [416, 'Requested range not satisfiable'],
|
100
|
+
ExpectationFailed => [417, 'Expectation failed'],
|
101
|
+
UnprocessableEntity => [422, 'Unprocessable entity'],
|
102
|
+
InternalServerError => [500, 'Internal server error'],
|
103
|
+
NotImplemented => [501, 'Not implemented'],
|
104
|
+
BadGateway => [502, 'Bad gateway'],
|
105
|
+
ServiceUnavailable => [503, 'Service unavailable'],
|
106
|
+
GatewayTimeout => [504, 'Gateway timeout'],
|
107
|
+
}.freeze
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Pliny::Extensions
|
2
|
+
module Instruments
|
3
|
+
def self.registered(app)
|
4
|
+
app.before do
|
5
|
+
@request_start = Time.now
|
6
|
+
Pliny.log(
|
7
|
+
instrumentation: true,
|
8
|
+
at: "start",
|
9
|
+
method: request.request_method,
|
10
|
+
path: request.path_info,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
app.after do
|
15
|
+
Pliny.log(
|
16
|
+
instrumentation: true,
|
17
|
+
at: "finish",
|
18
|
+
method: request.request_method,
|
19
|
+
path: request.path_info,
|
20
|
+
route_signature: route_signature,
|
21
|
+
status: status,
|
22
|
+
elapsed: (Time.now - @request_start).to_f
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
app.helpers do
|
27
|
+
def route_signature
|
28
|
+
env["ROUTE_SIGNATURE"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def route(verb, path, *)
|
34
|
+
condition { env["ROUTE_SIGNATURE"] = path.to_s }
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/pliny/log.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Pliny
|
2
|
+
module Log
|
3
|
+
def log(data, &block)
|
4
|
+
data = log_context.merge(data)
|
5
|
+
log_to_stream(stdout || $stdout, data, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def stdout=(stream)
|
9
|
+
@stdout = stream
|
10
|
+
end
|
11
|
+
|
12
|
+
def stdout
|
13
|
+
@stdout
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def log_context
|
19
|
+
RequestStore.store[:log_context] || {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_to_stream(stream, data, &block)
|
23
|
+
unless block
|
24
|
+
str = unparse(data)
|
25
|
+
if RUBY_PLATFORM == "java"
|
26
|
+
stream.puts str
|
27
|
+
else
|
28
|
+
mtx.synchronize { stream.puts str }
|
29
|
+
end
|
30
|
+
else
|
31
|
+
data = data.dup
|
32
|
+
start = Time.now
|
33
|
+
log_to_stream(stream, data.merge(at: "start"))
|
34
|
+
begin
|
35
|
+
res = yield
|
36
|
+
log_to_stream(stream, data.merge(
|
37
|
+
at: "finish", elapsed: (Time.now - start).to_f))
|
38
|
+
res
|
39
|
+
rescue
|
40
|
+
log_to_stream(stream, data.merge(
|
41
|
+
at: "exception", elapsed: (Time.now - start).to_f))
|
42
|
+
raise
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def mtx
|
48
|
+
@mtx ||= Mutex.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def quote_string(k, v)
|
52
|
+
# try to find a quote style that fits
|
53
|
+
if !v.include?('"')
|
54
|
+
%{#{k}="#{v}"}
|
55
|
+
elsif !v.include?("'")
|
56
|
+
%{#{k}='#{v}'}
|
57
|
+
else
|
58
|
+
%{#{k}="#{v.gsub(/"/, '\\"')}"}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def unparse(attrs)
|
63
|
+
attrs.map { |k, v| unparse_pair(k, v) }.compact.join(" ")
|
64
|
+
end
|
65
|
+
|
66
|
+
def unparse_pair(k, v)
|
67
|
+
v = v.call if v.is_a?(Proc)
|
68
|
+
# only quote strings if they include whitespace
|
69
|
+
if v == nil
|
70
|
+
nil
|
71
|
+
elsif v == true
|
72
|
+
k
|
73
|
+
elsif v.is_a?(Float)
|
74
|
+
"#{k}=#{format("%.3f", v)}"
|
75
|
+
elsif v.is_a?(String) && v =~ /\s/
|
76
|
+
quote_string(k, v)
|
77
|
+
elsif v.is_a?(Time)
|
78
|
+
"#{k}=#{v.iso8601}"
|
79
|
+
else
|
80
|
+
"#{k}=#{v}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|