pliny 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|