mangledotdev 0.1.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/lib/mangledotdev.rb +419 -0
- metadata +41 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a3e6f62d683b5a119aa43e4ec953e29b837019d5ccdfc24bcd319e2597181f5c
|
|
4
|
+
data.tar.gz: de1edd7d42fec313777ad5fe28aa6edf7389c2941dbc4219c0601334c3e39fde
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f993e4e3917a0311c6be42b72a61a98b2e76bdae80e47db9595e2ee8b57424b70872186504d2b7883969a49de0b56c61fb5691127b3d3dbfc7b64a468df74136
|
|
7
|
+
data.tar.gz: 54182dfc4c70ce26b9242996a1825a5b391abfc4174a4df6bb4154eac640ee0deddbe2d8d7d6d4cddc726d98f6d432dc6df3474f7174e2b931f5e4e9d1d7401d
|
data/lib/mangledotdev.rb
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
# InputManager - Manages sending requests to other processes and handling responses.
|
|
7
|
+
#
|
|
8
|
+
# This is an instance-based class - create one instance per request.
|
|
9
|
+
#
|
|
10
|
+
# Attributes:
|
|
11
|
+
# response (Hash): Complete response with status, data, errors, warnings
|
|
12
|
+
#
|
|
13
|
+
# Methods:
|
|
14
|
+
# request(): Send a request to another process
|
|
15
|
+
# get_response(): Get the full response object
|
|
16
|
+
# get_data(): Get the response data (returns nil on error)
|
|
17
|
+
class InputManager
|
|
18
|
+
attr_reader :response
|
|
19
|
+
|
|
20
|
+
# Generate a unique key for request/response matching.
|
|
21
|
+
# @return [String] Unique key
|
|
22
|
+
def self.gen_key
|
|
23
|
+
SecureRandom.hex(16)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Initialize a new InputManager instance.
|
|
27
|
+
def initialize
|
|
28
|
+
@process = nil
|
|
29
|
+
@raw_request = nil
|
|
30
|
+
@request = nil
|
|
31
|
+
@response = nil
|
|
32
|
+
@data = nil
|
|
33
|
+
@key = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Validate file and build command to execute.
|
|
39
|
+
#
|
|
40
|
+
# @param language [String] Programming language/runtime
|
|
41
|
+
# @param file [String] Path to file to execute
|
|
42
|
+
# @return [Array<String>] Command array for subprocess
|
|
43
|
+
# @raise [ArgumentError] Invalid file extension, file not found, or permission error
|
|
44
|
+
def get_command(language, file)
|
|
45
|
+
lang_upper = language.to_s.upcase
|
|
46
|
+
|
|
47
|
+
# On Windows, convert forward slashes to backslashes for file system operations
|
|
48
|
+
if Gem.win_platform?
|
|
49
|
+
file = file.gsub('/', '\\')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
file_ext = File.extname(file).downcase
|
|
53
|
+
|
|
54
|
+
# Extension validation - FIRST before file existence check
|
|
55
|
+
extension_map = {
|
|
56
|
+
'PYTHON' => ['.py'],
|
|
57
|
+
'PY' => ['.py'],
|
|
58
|
+
'JAVASCRIPT' => ['.js'],
|
|
59
|
+
'JS' => ['.js'],
|
|
60
|
+
'NODE' => ['.js'],
|
|
61
|
+
'NODEJS' => ['.js'],
|
|
62
|
+
'RUBY' => ['.rb'],
|
|
63
|
+
'RB' => ['.rb'],
|
|
64
|
+
'C' => ['.c', '.out', '.exe', ''],
|
|
65
|
+
'CS' => ['.exe', '.dll', ''],
|
|
66
|
+
'CPP' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
|
|
67
|
+
'C#' => ['.exe', '.dll', ''],
|
|
68
|
+
'C++' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
|
|
69
|
+
'CSHARP' => ['.exe', '.dll', ''],
|
|
70
|
+
'CPLUSPLUS' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
|
|
71
|
+
'EXE' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
|
|
72
|
+
'JAR' => ['.jar'],
|
|
73
|
+
'JAVA' => ['.jar'],
|
|
74
|
+
'RUST' => ['.rs', '.exe', '.out', ''],
|
|
75
|
+
'RS' => ['.rs', '.exe', '.out', ''],
|
|
76
|
+
'GO' => ['.go', '.exe', '.out', ''],
|
|
77
|
+
'GOLANG' => ['.go', '.exe', '.out', '']
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if extension_map.key?(lang_upper)
|
|
81
|
+
valid_extensions = extension_map[lang_upper]
|
|
82
|
+
unless valid_extensions.include?(file_ext)
|
|
83
|
+
expected = valid_extensions.map { |ext| ext.empty? ? '(no extension)' : ext }.join(', ')
|
|
84
|
+
raise ArgumentError, "Invalid file '#{file}' for language '#{language}'. Expected: e.g. 'file#{expected}'"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# File existence check
|
|
89
|
+
raise ArgumentError, "File not found: #{file}" unless File.exist?(file)
|
|
90
|
+
raise ArgumentError, "Path is not a file: #{file}" unless File.file?(file)
|
|
91
|
+
|
|
92
|
+
# Permission checks
|
|
93
|
+
compiled_languages = ['C', 'CS', 'CPP', 'C#', 'C++', 'CSHARP', 'CPLUSPLUS', 'EXE', 'RUST', 'RS', 'GO', 'GOLANG']
|
|
94
|
+
if compiled_languages.include?(lang_upper)
|
|
95
|
+
raise ArgumentError, "File is not executable: #{file}" unless File.executable?(file)
|
|
96
|
+
else
|
|
97
|
+
raise ArgumentError, "File is not readable: #{file}" unless File.readable?(file)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Auto-add ./ for compiled executables if not present and not absolute path
|
|
101
|
+
if compiled_languages.include?(lang_upper) && !File.absolute_path?(file) && !file.start_with?('./')
|
|
102
|
+
file = './' + file
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build command
|
|
106
|
+
lang_map = {
|
|
107
|
+
'PYTHON' => ['python', file],
|
|
108
|
+
'PY' => ['python', file],
|
|
109
|
+
'JAVASCRIPT' => ['node', file],
|
|
110
|
+
'JS' => ['node', file],
|
|
111
|
+
'NODE' => ['node', file],
|
|
112
|
+
'NODEJS' => ['node', file],
|
|
113
|
+
'RUBY' => ['ruby', file],
|
|
114
|
+
'RB' => ['ruby', file],
|
|
115
|
+
'C' => [file],
|
|
116
|
+
'CS' => file_ext == '.dll' ? ['dotnet', file] : [file],
|
|
117
|
+
'CPP' => [file],
|
|
118
|
+
'C#' => file_ext == '.dll' ? ['dotnet', file] : [file],
|
|
119
|
+
'C++' => [file],
|
|
120
|
+
'CSHARP' => file_ext == '.dll' ? ['dotnet', file] : [file],
|
|
121
|
+
'CPLUSPLUS' => [file],
|
|
122
|
+
'EXE' => [file],
|
|
123
|
+
'JAR' => ['java', '-jar', file],
|
|
124
|
+
'JAVA' => ['java', '-jar', file],
|
|
125
|
+
'RUST' => [file],
|
|
126
|
+
'RS' => [file],
|
|
127
|
+
'GO' => file_ext == '.go' ? ['go', 'run', file] : [file],
|
|
128
|
+
'GOLANG' => file_ext == '.go' ? ['go', 'run', file] : [file]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, "Unsupported language: #{language}" unless lang_map.key?(lang_upper)
|
|
132
|
+
|
|
133
|
+
lang_map[lang_upper]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
public
|
|
137
|
+
|
|
138
|
+
# Send a request to another process.
|
|
139
|
+
#
|
|
140
|
+
# @param is_unique [Boolean] Expect single output (true) or multiple (false)
|
|
141
|
+
# @param optional_output [Boolean] Output is optional (true) or required (false)
|
|
142
|
+
# @param data [Object] Data to send (any JSON-serializable type)
|
|
143
|
+
# @param language [String] Target language/runtime
|
|
144
|
+
# @param file [String] Path to target file
|
|
145
|
+
#
|
|
146
|
+
# Sets @response (Hash) with keys:
|
|
147
|
+
# - request_status (Boolean|nil): Success status
|
|
148
|
+
# - data: Response data (preserves type)
|
|
149
|
+
# - optionalOutput (Boolean): Echo of parameter
|
|
150
|
+
# - isUnique (Boolean): Echo of parameter
|
|
151
|
+
# - warnings (Array<String>): Warning messages
|
|
152
|
+
# - errors (Array<String>): Error messages
|
|
153
|
+
def request(is_unique: true, optional_output: true, data: nil, language:, file:)
|
|
154
|
+
begin
|
|
155
|
+
@key = InputManager.gen_key
|
|
156
|
+
command = get_command(language, file)
|
|
157
|
+
|
|
158
|
+
@raw_request = {
|
|
159
|
+
key: @key,
|
|
160
|
+
optionalOutput: optional_output,
|
|
161
|
+
isUnique: is_unique,
|
|
162
|
+
data: data
|
|
163
|
+
}
|
|
164
|
+
@request = JSON.generate(@raw_request)
|
|
165
|
+
|
|
166
|
+
response = {
|
|
167
|
+
request_status: nil,
|
|
168
|
+
data: nil,
|
|
169
|
+
optionalOutput: optional_output,
|
|
170
|
+
isUnique: is_unique,
|
|
171
|
+
warnings: [],
|
|
172
|
+
errors: []
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
stdout, stderr, status = Open3.capture3(*command, stdin_data: @request)
|
|
176
|
+
|
|
177
|
+
unless status.success?
|
|
178
|
+
response[:request_status] = false
|
|
179
|
+
response[:errors] << "Process exited with code #{status.exitstatus}"
|
|
180
|
+
response[:errors] << "stderr: #{stderr.strip}" unless stderr.strip.empty?
|
|
181
|
+
response[:warnings] << "Warning: these kind of errors result from an error in the targeted script."
|
|
182
|
+
@response = response
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
@response_data = []
|
|
187
|
+
stdout.strip.split("\n").each do |line|
|
|
188
|
+
next if line.strip.empty?
|
|
189
|
+
|
|
190
|
+
begin
|
|
191
|
+
parsed_data = JSON.parse(line)
|
|
192
|
+
|
|
193
|
+
# Validate response has matching key or null key (for init errors)
|
|
194
|
+
# This ensures we only process responses meant for this request
|
|
195
|
+
if parsed_data.is_a?(Hash) && (parsed_data['key'] == @key || parsed_data['key'].nil?)
|
|
196
|
+
@response_data << parsed_data
|
|
197
|
+
end
|
|
198
|
+
rescue JSON::ParserError
|
|
199
|
+
# Ignore lines that aren't valid JSON (e.g., debug prints)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if @response_data.length != 0
|
|
204
|
+
failure = false
|
|
205
|
+
@response_data.each do |resp|
|
|
206
|
+
failure = true unless resp['request_status']
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
response[:request_status] = !failure
|
|
210
|
+
|
|
211
|
+
response[:isUnique] = @response_data[0]['isUnique']
|
|
212
|
+
|
|
213
|
+
@response_data.each do |resp|
|
|
214
|
+
resp['errors'].each do |err|
|
|
215
|
+
response[:errors] << err
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
data_list = []
|
|
220
|
+
@response_data.each do |resp|
|
|
221
|
+
data_list << resp['data']
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if response[:isUnique]
|
|
225
|
+
if data_list.length == 1
|
|
226
|
+
response[:data] = data_list[0]
|
|
227
|
+
else
|
|
228
|
+
response[:request_status] = false
|
|
229
|
+
response[:data] = nil
|
|
230
|
+
response[:errors] << "Error: Expected 1 output (isUnique=True) but received #{data_list.length}."
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
response[:data] = data_list
|
|
234
|
+
end
|
|
235
|
+
elsif optional_output
|
|
236
|
+
response[:request_status] = nil
|
|
237
|
+
response[:data] = nil
|
|
238
|
+
response[:warnings] << "Warning: the output setting is set to optional, and the targeted program didn't gave any output."
|
|
239
|
+
else
|
|
240
|
+
response[:request_status] = false
|
|
241
|
+
response[:data] = nil
|
|
242
|
+
response[:errors] << "Error: OutputManager might not be used or not correctly."
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
@response = response
|
|
246
|
+
|
|
247
|
+
rescue ArgumentError => e
|
|
248
|
+
@response = {
|
|
249
|
+
request_status: false,
|
|
250
|
+
data: nil,
|
|
251
|
+
optionalOutput: optional_output,
|
|
252
|
+
isUnique: is_unique,
|
|
253
|
+
warnings: ["Warning: targeted file not found or can't be executed, consider checking file informations and language dependencies."],
|
|
254
|
+
errors: ["Error: #{e.message}"]
|
|
255
|
+
}
|
|
256
|
+
rescue => e
|
|
257
|
+
@response = {
|
|
258
|
+
request_status: false,
|
|
259
|
+
data: nil,
|
|
260
|
+
optionalOutput: optional_output,
|
|
261
|
+
isUnique: is_unique,
|
|
262
|
+
warnings: [],
|
|
263
|
+
errors: ["Unexpected error: #{e.message}"]
|
|
264
|
+
}
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Get the full response object.
|
|
269
|
+
#
|
|
270
|
+
# @return [Hash] The complete response with status, data, errors, warnings.
|
|
271
|
+
def get_response
|
|
272
|
+
@response
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Get the response data if request was successful.
|
|
276
|
+
#
|
|
277
|
+
# @return [Object, nil] The data from the response (any type), or nil if request failed.
|
|
278
|
+
# The return type matches the type sent by the target process.
|
|
279
|
+
def get_data
|
|
280
|
+
if @response
|
|
281
|
+
return @response[:data] if @response[:request_status]
|
|
282
|
+
end
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Bundle any value for use with request() - for API consistency with other languages.
|
|
287
|
+
# In Ruby, this just returns the value as-is since Ruby handles serialization automatically.
|
|
288
|
+
#
|
|
289
|
+
# @param value [Object] Any value
|
|
290
|
+
# @return [Object] The same value (Ruby handles JSON serialization automatically)
|
|
291
|
+
def self.bundle(value)
|
|
292
|
+
value
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# OutputManager - Manages receiving requests from other processes and sending responses.
|
|
297
|
+
#
|
|
298
|
+
# This is a class-based/static manager - all methods are class methods.
|
|
299
|
+
# Must call init() before using.
|
|
300
|
+
#
|
|
301
|
+
# Class Attributes:
|
|
302
|
+
# data: The request data (accessible after init())
|
|
303
|
+
#
|
|
304
|
+
# Class Methods:
|
|
305
|
+
# init(): Initialize and read request from stdin
|
|
306
|
+
# output(val): Send response back via stdout
|
|
307
|
+
class OutputManager
|
|
308
|
+
@@original_stdout = nil
|
|
309
|
+
@@request = nil
|
|
310
|
+
@@data = nil
|
|
311
|
+
@@request_status = nil
|
|
312
|
+
@@optional = nil
|
|
313
|
+
@@unique_state = nil
|
|
314
|
+
@@init_error = nil
|
|
315
|
+
@@errors = []
|
|
316
|
+
@@warnings = []
|
|
317
|
+
|
|
318
|
+
class << self
|
|
319
|
+
attr_accessor :data
|
|
320
|
+
|
|
321
|
+
# Initialize OutputManager and read request from stdin.
|
|
322
|
+
#
|
|
323
|
+
# Must be called before using output() or accessing data.
|
|
324
|
+
# Suppresses stdout to prevent pollution of JSON protocol.
|
|
325
|
+
def init
|
|
326
|
+
# Save original stdout so we can restore it later
|
|
327
|
+
@@original_stdout = $stdout
|
|
328
|
+
|
|
329
|
+
# Redirect stdout to StringIO to suppress all puts/print statements
|
|
330
|
+
$stdout = StringIO.new
|
|
331
|
+
|
|
332
|
+
# Read the entire stdin (the JSON request from InputManager)
|
|
333
|
+
@@request = $stdin.read
|
|
334
|
+
@@data = JSON.parse(@@request)
|
|
335
|
+
self.data = @@data['data']
|
|
336
|
+
@@optional = @@data['optionalOutput']
|
|
337
|
+
|
|
338
|
+
# Reset state for new request
|
|
339
|
+
@@errors = []
|
|
340
|
+
@@warnings = []
|
|
341
|
+
@@init_error = nil
|
|
342
|
+
@@request_status = nil
|
|
343
|
+
@@unique_state = nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Send response back to the calling process.
|
|
347
|
+
#
|
|
348
|
+
# @param val [Object] Data to send (any JSON-serializable type)
|
|
349
|
+
#
|
|
350
|
+
# Note:
|
|
351
|
+
# Can be called multiple times if isUnique=false in request.
|
|
352
|
+
# Will error if called multiple times when isUnique=true.
|
|
353
|
+
def output(val)
|
|
354
|
+
# Check if OutputManager was initialized
|
|
355
|
+
if @@data.nil?
|
|
356
|
+
unless @@init_error
|
|
357
|
+
$stdout = @@original_stdout if @@original_stdout
|
|
358
|
+
|
|
359
|
+
@@request_status = false
|
|
360
|
+
@@errors << "Error: OutputManager isn't initialized."
|
|
361
|
+
write_output(nil, @@data)
|
|
362
|
+
@@init_error = true
|
|
363
|
+
end
|
|
364
|
+
else
|
|
365
|
+
# Check if we can output based on isUnique setting
|
|
366
|
+
# unique_state tracks if we've already output once
|
|
367
|
+
if !@@unique_state || !@@data['isUnique']
|
|
368
|
+
@@request_status = true
|
|
369
|
+
write_output(val, @@data)
|
|
370
|
+
else
|
|
371
|
+
# Multiple outputs when isUnique=true is an error
|
|
372
|
+
@@request_status = false
|
|
373
|
+
@@errors << "Error: outputs out of bound (isUnique: #{@@unique_state})."
|
|
374
|
+
write_output(val, @@data)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Mark that we've output once
|
|
378
|
+
@@unique_state = @@data['isUnique']
|
|
379
|
+
|
|
380
|
+
# Re-suppress stdout after writing response
|
|
381
|
+
$stdout = StringIO.new
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Bundle any value for use with output() - for API consistency with other languages.
|
|
386
|
+
# In Ruby, this just returns the value as-is since Ruby handles serialization automatically.
|
|
387
|
+
#
|
|
388
|
+
# @param value [Object] Any value
|
|
389
|
+
# @return [Object] The same value (Ruby handles JSON serialization automatically)
|
|
390
|
+
def bundle(value)
|
|
391
|
+
value
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
private
|
|
395
|
+
|
|
396
|
+
# Internal method to write JSON response to stdout.
|
|
397
|
+
#
|
|
398
|
+
# @param args [Object] Data to send in response
|
|
399
|
+
# @param _data [Hash] Request data for key/metadata
|
|
400
|
+
def write_output(args, _data)
|
|
401
|
+
# Restore original stdout to actually write the response
|
|
402
|
+
$stdout = @@original_stdout if @@original_stdout
|
|
403
|
+
|
|
404
|
+
# Build and write JSON response
|
|
405
|
+
response = {
|
|
406
|
+
key: _data ? _data['key'] : nil,
|
|
407
|
+
request_status: @@request_status,
|
|
408
|
+
data: args,
|
|
409
|
+
optionalOutput: @@optional,
|
|
410
|
+
isUnique: _data ? _data['isUnique'] : nil,
|
|
411
|
+
errors: @@errors,
|
|
412
|
+
warnings: @@warnings
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
puts JSON.generate(response)
|
|
416
|
+
$stdout.flush
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mangledotdev
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wass B.
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: The universal programming language communication system.
|
|
13
|
+
email:
|
|
14
|
+
- wass.b@proton.me
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/mangledotdev.rb
|
|
20
|
+
homepage: https://github.com/WassBe/mangle.dev
|
|
21
|
+
licenses:
|
|
22
|
+
- Apache-2.0
|
|
23
|
+
metadata: {}
|
|
24
|
+
rdoc_options: []
|
|
25
|
+
require_paths:
|
|
26
|
+
- lib
|
|
27
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
28
|
+
requirements:
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '0'
|
|
32
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
33
|
+
requirements:
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '0'
|
|
37
|
+
requirements: []
|
|
38
|
+
rubygems_version: 4.0.3
|
|
39
|
+
specification_version: 4
|
|
40
|
+
summary: Mangle.dev for Ruby.
|
|
41
|
+
test_files: []
|