functions_framework 0.2.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3591e5ab03b6e29d0a7f9b9c16d4d470138fbf0ddebec875e80f5f1df8a9767
4
- data.tar.gz: 50ed98f079c42638fcb3328bab3d1388e323613ea310842bcd3d887b1a96fdfb
3
+ metadata.gz: 817129c1773079fed039152996a18e91a054241940b8d6e3437cb66e10046304
4
+ data.tar.gz: ff3f61597726b60241e8b528ef520641fa3f08ddb7adcdfb32f9413fcb125f45
5
5
  SHA512:
6
- metadata.gz: 87a9631c32f05b5e5831f50e9dd7235351fd6556fb22474d655a916ceacf1d57e1d25a60369c31aa3790a9667cafec02dafad2ccf9c98499a681badf2516d13e
7
- data.tar.gz: 755d890b0684d2af126306c3b7f7f2e5d3b1392b0fb88e325b45eba6c85d9d6cc28e320e20a8a9d716244e52e63fee839ab02ec56014873c62dd77f3fe4ea37c
6
+ metadata.gz: 7396117d6e94bec87c70c697c7364b9256c7575f61c7259eb25df65694e9627b6c33a93e0caaea5d9d7955707e731eaddc27b90d4595f0029d181fc6cc18bfdf
7
+ data.tar.gz: 8eb929ace2a49ae764f5de0fef6dd7e0d668c80864368ad2379d0e52a89a44c7a3514e92dc02802d940374808a271a89f3076f1c8f3d0a532204118615e0c1af
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ### v0.4.1 / 2020-07-08
4
+
5
+ * Fixed unsupported signal error on Windows.
6
+ * Fixed several edge case errors in legacy event conversion.
7
+ * Generated Content-Type headers now properly quote param values if needed.
8
+ * Minor documentation updates.
9
+
10
+ ### v0.4.0 / 2020-06-29
11
+
12
+ * Dropped the legacy and largely unsupported `:event` function type. All event functions should be of type `:cloud_event`.
13
+ * Define the object context for function execution, and include an extensible context helper.
14
+ * Support for CloudEvents with specversion 0.3.
15
+ * CloudEvents now correct percent-encodes/decodes binary headers.
16
+ * CloudEvents now includes more robust RFC 2045 parsing of the Content-Type header.
17
+ * The CloudEventsError class now properly subclasses StandardError instead of RuntimeError.
18
+ * Removed redundant `_string` accessors from event classes since raw forms are already available via `[]`.
19
+ * A variety of corrections to event-related class documentation.
20
+
21
+ ### v0.3.1 / 2020-06-27
22
+
23
+ * Fixed crash when using "return" directly in a function block.
24
+ * Added a more flexible request generation helper in the testing module.
25
+ * Fixed several typos in the documentation.
26
+
27
+ ### v0.3.0 / 2020-06-26
28
+
29
+ * Updated the CloudEvent data format for converted pubsub events to conform to Cloud Run's conversion.
30
+
31
+ ### v0.2.1 / 2020-06-25
32
+
33
+ * The `--signature-type` check recognizes the legacy `event` type for `:cloud_event` functions.
34
+
3
35
  ### v0.2.0 / 2020-06-24
4
36
 
5
37
  Significant changes:
data/README.md CHANGED
@@ -60,7 +60,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
60
60
  ```ruby
61
61
  # Gemfile
62
62
  source "https://rubygems.org"
63
- gem "functions_framework", "~> 0.2"
63
+ gem "functions_framework", "~> 0.4"
64
64
  ```
65
65
 
66
66
  Create a file called `app.rb` and include the following code. This defines a
@@ -88,7 +88,7 @@ bundle exec functions-framework-ruby --target hello
88
88
  In a separate shell, you can send requests to this function using curl:
89
89
 
90
90
  ```sh
91
- curl https://localhost:8080
91
+ curl http://localhost:8080
92
92
  # Output: Hello, world!
93
93
  ```
94
94
 
@@ -30,12 +30,21 @@ Google Cloud Functions is Google's scalable pay-as-you-go Functions-as-a-Service
30
30
  Functions Framework is designed especially for functions that can be hosted on
31
31
  Cloud Functions.
32
32
 
33
+ You can run Ruby functions on Google Cloud Functions by selecting the `ruby26`
34
+ runtime. This runtime uses a recent release of Ruby 2.6. Support for other
35
+ versions of Ruby may be added in the future.
36
+
37
+ > **Note:** Ruby support on Cloud Functions is currently in limited preview.
38
+ > It is not yet suitable for production workloads, and support is best-effort
39
+ > only. Access is currently limited to selected early-access users.
40
+
33
41
  ### Deploying and updating your function
34
42
 
35
43
  Before you can deploy to Cloud Functions, make sure your bundle, and in
36
44
  particular your `Gemfile.lock` file, is up to date. The easiest way to do this
37
45
  is to `bundle install` or `bundle update` and run your local tests prior to
38
- deploying.
46
+ deploying. Cloud Functions will not accept your function unless an up-to-date
47
+ `Gemfile.lock` is present.
39
48
 
40
49
  Choose a name for your function. This function name is how it will appear in the
41
50
  cloud console, and will also be part of the function's URL. (It's different from
@@ -96,6 +105,12 @@ to adapt it if you have an Anthos installation.
96
105
 
97
106
  ### Building an image for your function
98
107
 
108
+ Before you can deploy to Cloud Run, make sure your bundle, and in
109
+ particular your `Gemfile.lock` file, is up to date. The easiest way to do this
110
+ is to `bundle install` or `bundle update` and run your local tests prior to
111
+ deploying. The configuration used in the Dockerfile below will not accept your
112
+ function unless an up-to-date `Gemfile.lock` is present.
113
+
99
114
  First, build a Docker image containing your function. Following is a simple
100
115
  Dockerfile that you can use as a starting point. Feel free to adjust it to the
101
116
  needs of your project:
@@ -126,6 +141,10 @@ You must use your project ID, but you can choose an app name and build ID. The
126
141
  command may ask you for permission to enable the Cloud Build API for the project
127
142
  if it isn't already enabled.
128
143
 
144
+ Because you provide your own Docker image when deploying to Cloud Run, you can
145
+ use any version of Ruby supported by the Functions Framework, from 2.4 through
146
+ 2.7.
147
+
129
148
  ### Deploying an image to Cloud Run
130
149
 
131
150
  To deploy to Cloud Run, specify the same image URL that you built above. For
@@ -152,7 +171,7 @@ deployed function.
152
171
 
153
172
  Note that our Dockerfile's entrypoint did not pass any source file or target
154
173
  name to the Functions Framework. If these are not specified, the Framework will
155
- use the source `.app.rb` and the target `function` by default. To use different
174
+ use the source `./app.rb` and the target `function` by default. To use different
156
175
  values, you need to set the appropriate environment variables when deploying, as
157
176
  illustrated above with the `FUNCTION_SOURCE` and `FUNCTION_TARGET` variables.
158
177
 
@@ -64,7 +64,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
64
64
  ```ruby
65
65
  # Gemfile
66
66
  source "https://rubygems.org"
67
- gem "functions_framework", "~> 0.2"
67
+ gem "functions_framework", "~> 0.4"
68
68
  ```
69
69
 
70
70
  Create a file called `app.rb` and include the following code. This defines a
@@ -92,7 +92,7 @@ bundle exec functions-framework-ruby --target hello
92
92
  In a separate shell, you can send requests to this function using curl:
93
93
 
94
94
  ```sh
95
- curl https://localhost:8080
95
+ curl http://localhost:8080
96
96
  # Output: Hello, world!
97
97
  ```
98
98
 
@@ -101,7 +101,7 @@ Command-line flag | Environment variable | Description
101
101
  `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080`.
102
102
  `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function`.
103
103
  `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `app.rb` (in the current working directory).
104
- `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | Verifies that the function has the expected signature. Allowed values: `http` or `cloudevent`.
104
+ `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | Verifies that the function has the expected signature. Allowed values: `http`, `event`, or `cloudevent`.
105
105
  `--environment` | `RACK_ENV` | Sets the Rack environment.
106
106
  `--bind` | `FUNCTION_BIND_ADDR` | Binds to the given address. Default: `0.0.0.0`.
107
107
  `--min-threads` | `FUNCTION_MIN_THREADS` | Sets the minimum thread pool size, overriding Puma's default.
@@ -30,7 +30,7 @@ that returns a simple message in the HTTP response body:
30
30
  ```ruby
31
31
  require "functions_framework"
32
32
 
33
- FunctionsFramework.http("hello") do |request|
33
+ FunctionsFramework.http "hello" do |request|
34
34
  # Return the response body.
35
35
  "Hello, world!\n"
36
36
  end
@@ -43,7 +43,7 @@ now cover these in a bit more detail.
43
43
 
44
44
  An HTTP function is passed a request, which is an object of type
45
45
  [Rack::Request](https://rubydoc.info/gems/rack/Rack/Request). This object
46
- provides methods methods for obtaining request information such as the method,
46
+ provides methods for obtaining request information such as the method,
47
47
  path, query parameters, body content, and headers. You can also obtain the raw
48
48
  Rack environment using the `env` method. The following example includes some
49
49
  request information in the response:
@@ -51,7 +51,7 @@ request information in the response:
51
51
  ```ruby
52
52
  require "functions_framework"
53
53
 
54
- FunctionsFramework.http("request_info") do |request|
54
+ FunctionsFramework.http "request_info_example" do |request|
55
55
  # Include some request info in the response body.
56
56
  "Received #{request.method} from #{request.url}!\n"
57
57
  end
@@ -66,7 +66,7 @@ hosting environment.
66
66
  ```ruby
67
67
  require "functions_framework"
68
68
 
69
- FunctionsFramework.http("logging_example") do |request|
69
+ FunctionsFramework.http "logging_example" do |request|
70
70
  # Log some request info.
71
71
  request.logger.info "I received #{request.method} from #{request.url}!"
72
72
  # A simple response body.
@@ -106,10 +106,19 @@ framework such as Ruby on Rails, you may want to consider a solution such as
106
106
  Google Cloud Run that is tailored to larger applications. However, a lightweight
107
107
  framework such as Sinatra is sometimes useful when writing HTTP functions.
108
108
 
109
- It is easy to connect an HTTP function to a Sinatra app. Write the Sinatra app
110
- using the "modular" Sinatra interface (i.e. subclass `Sinatra::Base`), and then
111
- simply run the Sinatra app as a Rack handler from the function. Here is a basic
112
- example:
109
+ It is easy to connect an HTTP function to a Sinatra app. First, declare the
110
+ dependency on Sinatra in your `Gemfile`:
111
+
112
+ ```ruby
113
+ # Gemfile
114
+ source "https://rubygems.org"
115
+ gem "functions_framework", "~> 0.4"
116
+ gem "sinatra", "~> 2.0"
117
+ ```
118
+
119
+ Write the Sinatra app using the "modular" Sinatra interface (i.e. subclass
120
+ `Sinatra::Base`), and then run the Sinatra app directly as a Rack handler from
121
+ the function. Here is a basic example:
113
122
 
114
123
  ```ruby
115
124
  require "functions_framework"
@@ -143,20 +152,25 @@ information about it:
143
152
  ```ruby
144
153
  require "functions_framework"
145
154
 
146
- FunctionsFramework.cloud_event("hello") do |event|
155
+ FunctionsFramework.cloud_event "hello" do |event|
147
156
  FunctionsFramework.logger.info "I received an event of type #{event.type}!"
148
157
  end
149
158
  ```
150
159
 
151
- The event parameter is a
152
- [CloudEvents V1 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V1)
153
- object. You can find detailed information about the fields of a CloudEvent from
154
- the [CloudEvents spec](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
160
+ The event parameter will be either a
161
+ [CloudEvents V0.3 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V0)
162
+ object ([see spec](https://github.com/cloudevents/spec/blob/v0.3/spec.md)) or a
163
+ [CloudEvents V1.0 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V1)
164
+ object ([see spec](https://github.com/cloudevents/spec/blob/v1.0/spec.md)).
155
165
 
156
166
  Some Google Cloud services send events in a legacy event format that was defined
157
167
  prior to CloudEvents. The Functions Framework will convert these legacy events
158
- to an equivalent CloudEvents type, so your function will always receive a
159
- CloudEvent when it is sent an event from Google Cloud.
168
+ to an equivalent CloudEvents V1 type, so your function will always receive a
169
+ CloudEvent object when it is sent an event from Google Cloud. The precise
170
+ mapping between legacy events and CloudEvents is not specified in detail here,
171
+ but in general, the _data_ from the legacy event will be mapped to the `data`
172
+ field in the CloudEvent, and the _context_ from the legacy event will be mapped
173
+ to equivalent CloudEvent attributes.
160
174
 
161
175
  ## Error handling
162
176
 
@@ -175,7 +189,7 @@ HTTP response yourself. For example:
175
189
  ```ruby
176
190
  require "functions_framework"
177
191
 
178
- FunctionsFramework.http("error_reporter") do |request|
192
+ FunctionsFramework.http "error_reporter" do |request|
179
193
  begin
180
194
  raise "whoops!"
181
195
  rescue RuntimeError => e
@@ -188,9 +202,10 @@ end
188
202
 
189
203
  A Functions Framework based "project" or "application" is a typical Ruby
190
204
  application. It should include a `Gemfile` that specifies the gem dependencies
191
- (including the `functions_framework` gem itself), and at least one Ruby source
192
- file that defines functions. It can also include additional Ruby files defining
193
- classes and methods that assist in the function implementation.
205
+ (including the `functions_framework` gem itself), and any other dependencies
206
+ needed by the function. It must include at least one Ruby source file that
207
+ defines functions, and can also include additional Ruby files defining classes
208
+ and methods that assist in the function implementation.
194
209
 
195
210
  The "entrypoint" to the project, also called the "source", is a Ruby file. It
196
211
  can define any number of functions (with distinct names), although it is often
@@ -221,7 +236,7 @@ A simple project might look like this:
221
236
  ```ruby
222
237
  # Gemfile
223
238
  source "https://rubygems.org"
224
- gem "functions_framework", "~> 0.2"
239
+ gem "functions_framework", "~> 0.4"
225
240
  ```
226
241
 
227
242
  ```ruby
@@ -229,7 +244,7 @@ gem "functions_framework", "~> 0.2"
229
244
  require "functions_framework"
230
245
  require_relative "lib/hello"
231
246
 
232
- FunctionsFramework.http("hello") do |request|
247
+ FunctionsFramework.http "hello" do |request|
233
248
  Hello.new(request).build_response
234
249
  end
235
250
  ```
@@ -237,7 +252,7 @@ end
237
252
  ```ruby
238
253
  # lib/hello.rb
239
254
  class Hello
240
- def initialize(request)
255
+ def initialize request
241
256
  @request = request
242
257
  end
243
258
 
@@ -139,17 +139,6 @@ module FunctionsFramework
139
139
  self
140
140
  end
141
141
 
142
- ##
143
- # This is an obsolete interface that defines an event function taking two
144
- # arguments (data and context) rather than one.
145
- #
146
- # @deprecated Use {FunctionsFramework.cloud_event} instead.
147
- #
148
- def event name = DEFAULT_TARGET, &block
149
- global_registry.add_event name, &block
150
- self
151
- end
152
-
153
142
  ##
154
143
  # Define a function that responds to CloudEvents.
155
144
  #
@@ -137,7 +137,7 @@ module FunctionsFramework
137
137
  raise "Undefined function: #{@target.inspect}" if function.nil?
138
138
  unless @signature_type.nil? ||
139
139
  @signature_type == "http" && function.type == :http ||
140
- @signature_type == "cloudevent" && function.type == :cloud_event
140
+ ["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
141
141
  raise "Function #{@target.inspect} does not match type #{@signature_type}"
142
142
  end
143
143
  ::FunctionsFramework.start function do |config|
@@ -23,11 +23,13 @@ module FunctionsFramework
23
23
  # CloudEvents implementation.
24
24
  #
25
25
  # This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
26
- # [1.0 specification](https://github.com/cloudevents/spec/blob/master/spec.md).
26
+ # specification. It supports both
27
+ # [CloudEvents 0.3](https://github.com/cloudevents/spec/blob/v0.3/spec.md) and
28
+ # [CloudEvents 1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
27
29
  #
28
30
  module CloudEvents
29
31
  # @private
30
- SUPPORTED_SPEC_VERSIONS = ["1.0"].freeze
32
+ SUPPORTED_SPEC_VERSIONS = ["0.3", "1.0"].freeze
31
33
 
32
34
  class << self
33
35
  ##
@@ -23,29 +23,31 @@ module FunctionsFramework
23
23
  # Case-insensitive fields, such as media_type and subtype, are normalized
24
24
  # to lower case.
25
25
  #
26
+ # If parsing fails, this class will try to get as much information as it
27
+ # can, and fill the rest with defaults as recommended in RFC 2045 sec 5.2.
28
+ # In case of a parsing error, the {#error_message} field will be set.
29
+ #
26
30
  class ContentType
27
31
  ##
28
- # Parse the given header value
32
+ # Parse the given header value.
29
33
  #
30
34
  # @param string [String] Content-Type header value in RFC 2045 format
31
35
  #
32
36
  def initialize string
33
37
  @string = string
34
- # TODO: This handles simple cases but is not RFC-822 compliant.
35
- sections = string.to_s.split ";"
36
- media_type, subtype = sections.shift.split "/"
37
- subtype_prefix, subtype_format = subtype.split "+"
38
- @media_type = media_type.strip.downcase
39
- @subtype = subtype.strip.downcase
40
- @subtype_prefix = subtype_prefix.strip.downcase
41
- @subtype_format = subtype_format&.strip&.downcase
42
- @params = initialize_params sections
38
+ @media_type = "text"
39
+ @subtype_base = @subtype = "plain"
40
+ @subtype_format = nil
41
+ @params = []
42
+ @charset = "us-ascii"
43
+ @error_message = nil
44
+ parse consume_comments string.strip
43
45
  @canonical_string = "#{@media_type}/#{@subtype}" +
44
- @params.map { |k, v| "; #{k}=#{v}" }.join
46
+ @params.map { |k, v| "; #{k}=#{maybe_quote v}" }.join
45
47
  end
46
48
 
47
49
  ##
48
- # The original header content string
50
+ # The original header content string.
49
51
  # @return [String]
50
52
  #
51
53
  attr_reader :string
@@ -66,7 +68,7 @@ module FunctionsFramework
66
68
 
67
69
  ##
68
70
  # The entire content subtype (which could include an extension delimited
69
- # by a plus sign)
71
+ # by a plus sign).
70
72
  # @return [String]
71
73
  #
72
74
  attr_reader :subtype
@@ -75,7 +77,7 @@ module FunctionsFramework
75
77
  # The portion of the content subtype before any plus sign.
76
78
  # @return [String]
77
79
  #
78
- attr_reader :subtype_prefix
80
+ attr_reader :subtype_base
79
81
 
80
82
  ##
81
83
  # The portion of the content subtype after any plus sign, or nil if there
@@ -91,6 +93,18 @@ module FunctionsFramework
91
93
  #
92
94
  attr_reader :params
93
95
 
96
+ ##
97
+ # The charset, defaulting to "us-ascii" if none is explicitly set.
98
+ # @return [String]
99
+ #
100
+ attr_reader :charset
101
+
102
+ ##
103
+ # The error message when parsing, or `nil` if there was no error message.
104
+ # @return [String,nil]
105
+ #
106
+ attr_reader :error_message
107
+
94
108
  ##
95
109
  # An array of values for the given parameter name
96
110
  # @param key [String]
@@ -101,15 +115,6 @@ module FunctionsFramework
101
115
  @params.inject([]) { |a, (k, v)| key == k ? a << v : a }
102
116
  end
103
117
 
104
- ##
105
- # The first value of the "charset" parameter, or nil if there is no
106
- # charset.
107
- # @return [String,nil]
108
- #
109
- def charset
110
- param_values("charset").first
111
- end
112
-
113
118
  ## @private
114
119
  def == other
115
120
  other.is_a?(ContentType) && canonical_string == other.canonical_string
@@ -121,18 +126,96 @@ module FunctionsFramework
121
126
  canonical_string.hash
122
127
  end
123
128
 
129
+ ## @private
130
+ class ParseError < ::StandardError
131
+ end
132
+
124
133
  private
125
134
 
126
- def initialize_params sections
127
- params = sections.map do |s|
128
- k, v = s.split "="
129
- [k.strip.downcase, v.strip]
135
+ def parse str
136
+ @media_type, str = consume_token str, downcase: true, error_message: "Failed to parse media type"
137
+ str = consume_special str, "/"
138
+ @subtype, str = consume_token str, downcase: true, error_message: "Failed to parse subtype"
139
+ @subtype_base, @subtype_format = @subtype.split "+", 2
140
+ until str.empty?
141
+ str = consume_special str, ";"
142
+ name, str = consume_token str, downcase: true, error_message: "Faled to parse attribute name"
143
+ str = consume_special str, "=", error_message: "Failed to find value for attribute #{name}"
144
+ val, str = consume_token_or_quoted str, error_message: "Failed to parse value for attribute #{name}"
145
+ @params << [name, val]
146
+ @charset = val if name == "charset"
147
+ end
148
+ rescue ParseError => e
149
+ @error_message = e.message
150
+ end
151
+
152
+ def consume_token str, downcase: false, error_message: nil
153
+ match = /^([\w!#\$%&'\*\+\.\^`\{\|\}-]+)(.*)$/.match str
154
+ raise ParseError, error_message || "Expected token" unless match
155
+ token = match[1]
156
+ token.downcase! if downcase
157
+ str = consume_comments match[2].strip
158
+ [token, str]
159
+ end
160
+
161
+ def consume_special str, expected, error_message: nil
162
+ raise ParseError, error_message || "Expected #{expected.inspect}" unless str.start_with? expected
163
+ consume_comments str[1..-1].strip
164
+ end
165
+
166
+ def consume_token_or_quoted str, error_message: nil
167
+ return consume_token str unless str.start_with? '"'
168
+ arr = []
169
+ index = 1
170
+ loop do
171
+ char = str[index]
172
+ case char
173
+ when nil
174
+ raise ParseError, error_message || "Quoted-string never finished"
175
+ when "\""
176
+ break
177
+ when "\\"
178
+ char = str[index + 1]
179
+ raise ParseError, error_message || "Quoted-string never finished" unless char
180
+ arr << char
181
+ index += 2
182
+ else
183
+ arr << char
184
+ index += 1
185
+ end
130
186
  end
131
- params.sort! do |(k1, v1), (k2, v2)|
132
- a = k1 <=> k2
133
- a.zero? ? v1 <=> v2 : a
187
+ index += 1
188
+ str = consume_comments str[index..-1].strip
189
+ [arr.join, str]
190
+ end
191
+
192
+ def consume_comments str
193
+ return str unless str.start_with? "("
194
+ index = 1
195
+ loop do
196
+ char = str[index]
197
+ case char
198
+ when nil
199
+ raise ParseError, "Comment never finished"
200
+ when ")"
201
+ break
202
+ when "\\"
203
+ index += 2
204
+ when "("
205
+ str = consume_comments str[index..-1]
206
+ index = 0
207
+ else
208
+ index += 1
209
+ end
134
210
  end
135
- params
211
+ index += 1
212
+ consume_comments str[index..-1].strip
213
+ end
214
+
215
+ def maybe_quote str
216
+ return str if /^[\w!#\$%&'\*\+\.\^`\{\|\}-]+$/ =~ str
217
+ str = str.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
218
+ "\"#{str}\""
136
219
  end
137
220
  end
138
221
  end