protostar 0.1.0.beta1
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/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +647 -0
- data/THIRD_PARTY_NOTICES.md +61 -0
- data/exe/protoc-gen-protostar +7 -0
- data/exe/protostar +7 -0
- data/lib/protostar/builder/generator.rb +71 -0
- data/lib/protostar/builder/sink.rb +35 -0
- data/lib/protostar/builder.rb +965 -0
- data/lib/protostar/cli.rb +267 -0
- data/lib/protostar/codec/decode_generator.rb +199 -0
- data/lib/protostar/codec/decoder.rb +296 -0
- data/lib/protostar/codec/encode_generator.rb +224 -0
- data/lib/protostar/codec/encoder.rb +121 -0
- data/lib/protostar/codec/sizer.rb +91 -0
- data/lib/protostar/codec.rb +15 -0
- data/lib/protostar/compiler/ast.rb +193 -0
- data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
- data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
- data/lib/protostar/compiler/emitters/grpc.rb +175 -0
- data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
- data/lib/protostar/compiler/emitters/rbs.rb +394 -0
- data/lib/protostar/compiler/emitters/ruby.rb +503 -0
- data/lib/protostar/compiler/emitters.rb +268 -0
- data/lib/protostar/compiler/include/LICENSE +32 -0
- data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
- data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
- data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
- data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
- data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
- data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
- data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
- data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
- data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
- data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
- data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
- data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
- data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
- data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
- data/lib/protostar/compiler/lexer.rb +659 -0
- data/lib/protostar/compiler/linker.rb +894 -0
- data/lib/protostar/compiler/option_interpreter.rb +363 -0
- data/lib/protostar/compiler/parser.rb +1696 -0
- data/lib/protostar/compiler/retention.rb +104 -0
- data/lib/protostar/compiler/run.rb +184 -0
- data/lib/protostar/compiler/source_info.rb +161 -0
- data/lib/protostar/compiler/symbol_table.rb +223 -0
- data/lib/protostar/compiler/text_format.rb +340 -0
- data/lib/protostar/compiler/validator.rb +944 -0
- data/lib/protostar/compiler.rb +304 -0
- data/lib/protostar/diagnostics/codes.rb +1307 -0
- data/lib/protostar/diagnostics/formats.rb +64 -0
- data/lib/protostar/diagnostics/renderer.rb +240 -0
- data/lib/protostar/diagnostics/source.rb +153 -0
- data/lib/protostar/diagnostics.rb +117 -0
- data/lib/protostar/dsl.rb +367 -0
- data/lib/protostar/enum.rb +70 -0
- data/lib/protostar/errors.rb +23 -0
- data/lib/protostar/google/protobuf/any_pb.rb +90 -0
- data/lib/protostar/google/protobuf/api_pb.rb +199 -0
- data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
- data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
- data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
- data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
- data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
- data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
- data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
- data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
- data/lib/protostar/google/protobuf/type_pb.rb +202 -0
- data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
- data/lib/protostar/google_protobuf_shim.rb +24 -0
- data/lib/protostar/json/encoder.rb +340 -0
- data/lib/protostar/json/errors.rb +34 -0
- data/lib/protostar/json/parser.rb +437 -0
- data/lib/protostar/json/path.rb +39 -0
- data/lib/protostar/json.rb +95 -0
- data/lib/protostar/lint/command.rb +197 -0
- data/lib/protostar/lint/config.rb +98 -0
- data/lib/protostar/lint/context.rb +126 -0
- data/lib/protostar/lint/directives.rb +56 -0
- data/lib/protostar/lint/fixer.rb +165 -0
- data/lib/protostar/lint/naming.rb +54 -0
- data/lib/protostar/lint/rule.rb +120 -0
- data/lib/protostar/lint/rules/comment_enum.rb +31 -0
- data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
- data/lib/protostar/lint/rules/comment_field.rb +33 -0
- data/lib/protostar/lint/rules/comment_message.rb +31 -0
- data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
- data/lib/protostar/lint/rules/comment_service.rb +31 -0
- data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
- data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
- data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
- data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
- data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
- data/lib/protostar/lint/rules/field_not_required.rb +39 -0
- data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
- data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
- data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/import_no_public.rb +29 -0
- data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
- data/lib/protostar/lint/rules/import_used.rb +33 -0
- data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
- data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
- data/lib/protostar/lint/rules/no_groups.rb +34 -0
- data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/package_defined.rb +28 -0
- data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
- data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
- data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
- data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
- data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
- data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
- data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
- data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/service_suffix.rb +28 -0
- data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
- data/lib/protostar/lint/runner.rb +135 -0
- data/lib/protostar/lint.rb +122 -0
- data/lib/protostar/message/accessors.rb +86 -0
- data/lib/protostar/message/coercion.rb +171 -0
- data/lib/protostar/message/field_info.rb +145 -0
- data/lib/protostar/message/reflection.rb +149 -0
- data/lib/protostar/message.rb +572 -0
- data/lib/protostar/plugin.rb +94 -0
- data/lib/protostar/schema/default_value.rb +113 -0
- data/lib/protostar/schema/descriptor.rb +41 -0
- data/lib/protostar/schema/draft/copy.rb +88 -0
- data/lib/protostar/schema/draft/resolution.rb +177 -0
- data/lib/protostar/schema/draft.rb +779 -0
- data/lib/protostar/schema/enum.rb +106 -0
- data/lib/protostar/schema/extension_range.rb +37 -0
- data/lib/protostar/schema/features.rb +153 -0
- data/lib/protostar/schema/field.rb +201 -0
- data/lib/protostar/schema/file.rb +217 -0
- data/lib/protostar/schema/freezer.rb +61 -0
- data/lib/protostar/schema/message.rb +104 -0
- data/lib/protostar/schema/oneof.rb +38 -0
- data/lib/protostar/schema/options.rb +367 -0
- data/lib/protostar/schema/pool.rb +179 -0
- data/lib/protostar/schema/proto.rb +576 -0
- data/lib/protostar/schema/resolver.rb +118 -0
- data/lib/protostar/schema/service.rb +89 -0
- data/lib/protostar/schema/source_info.rb +53 -0
- data/lib/protostar/schema.rb +65 -0
- data/lib/protostar/short_names/any.rb +7 -0
- data/lib/protostar/short_names/duration.rb +7 -0
- data/lib/protostar/short_names/empty.rb +7 -0
- data/lib/protostar/short_names/field_mask.rb +7 -0
- data/lib/protostar/short_names/struct.rb +9 -0
- data/lib/protostar/short_names/timestamp.rb +7 -0
- data/lib/protostar/short_names/wrappers.rb +15 -0
- data/lib/protostar/text/extensions.rb +114 -0
- data/lib/protostar/text/parser.rb +439 -0
- data/lib/protostar/text/printer.rb +304 -0
- data/lib/protostar/text/tokenizer.rb +116 -0
- data/lib/protostar/text.rb +67 -0
- data/lib/protostar/types.rb +152 -0
- data/lib/protostar/unknown_field.rb +58 -0
- data/lib/protostar/version.rb +5 -0
- data/lib/protostar/view/generator.rb +298 -0
- data/lib/protostar/view/map.rb +194 -0
- data/lib/protostar/view/repeated.rb +143 -0
- data/lib/protostar/view/scan.rb +183 -0
- data/lib/protostar/view.rb +283 -0
- data/lib/protostar/well_known/any.rb +52 -0
- data/lib/protostar/well_known/duration.rb +53 -0
- data/lib/protostar/well_known/list_value.rb +63 -0
- data/lib/protostar/well_known/struct.rb +55 -0
- data/lib/protostar/well_known/timestamp.rb +64 -0
- data/lib/protostar/well_known/value.rb +60 -0
- data/lib/protostar/well_known.rb +38 -0
- data/lib/protostar/wire/io_source.rb +40 -0
- data/lib/protostar/wire/reader.rb +254 -0
- data/lib/protostar/wire/writer.rb +201 -0
- data/lib/protostar/wire.rb +96 -0
- data/lib/protostar.rb +95 -0
- data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
- data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
- data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
- data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
- data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
- data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
- data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
- data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
- data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
- data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
- data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
- data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
- data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
- data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
- data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
- data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
- data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
- data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
- data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
- data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
- data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
- data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
- data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
- data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
- data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
- data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
- data/sig/protostar/lint.rbs +216 -0
- data/sig/protostar.rbs +326 -0
- metadata +366 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module JSON
|
|
5
|
+
# Raised by everything in {JSON}
|
|
6
|
+
class Error < Protostar::Error; end
|
|
7
|
+
|
|
8
|
+
# Raised for JSON that does not describe a message of the class it is
|
|
9
|
+
# decoded into. {#path} names the field where the problem was found, as
|
|
10
|
+
# in `friends[2].name`; it is empty for the top-level value.
|
|
11
|
+
class ParseError < Error
|
|
12
|
+
# @return [String]
|
|
13
|
+
attr_reader :path
|
|
14
|
+
|
|
15
|
+
def initialize(message, path = "")
|
|
16
|
+
@path = path
|
|
17
|
+
super(path.empty? ? message : "#{path}: #{message}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Raised for a message whose contents have no JSON form: a Timestamp or
|
|
22
|
+
# Duration out of range, an Any whose type cannot be found, a Value with
|
|
23
|
+
# no kind set and the like
|
|
24
|
+
class EncodeError < Error
|
|
25
|
+
# @return [String]
|
|
26
|
+
attr_reader :path
|
|
27
|
+
|
|
28
|
+
def initialize(message, path = "")
|
|
29
|
+
@path = path
|
|
30
|
+
super(path.empty? ? message : "#{path}: #{message}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Protostar
|
|
6
|
+
module JSON
|
|
7
|
+
# Reads proto3 JSON into a message, accepting what google-protobuf's Ruby
|
|
8
|
+
# gem accepts. The stdlib parser builds the raw tree; this walks it
|
|
9
|
+
# against the message's fields.
|
|
10
|
+
# @api private
|
|
11
|
+
class Parser
|
|
12
|
+
F = Message::FieldInfo
|
|
13
|
+
SPECIAL = Encoder::SPECIAL
|
|
14
|
+
|
|
15
|
+
# Marks an enum name that is not known, dropped with ignore_unknown_fields
|
|
16
|
+
SKIP = Object.new.freeze
|
|
17
|
+
|
|
18
|
+
RANGES = Message::Coercion::RANGES
|
|
19
|
+
INT64_LIMIT = 2.0**63
|
|
20
|
+
UINT64_LIMIT = 2.0**64
|
|
21
|
+
QUOTED_INTEGER = /\A-?\d+\z/
|
|
22
|
+
# Integers in decimal or exponent form, read through a double as protoc does ("1e5")
|
|
23
|
+
QUOTED_INTEGRAL = /\A-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\z/
|
|
24
|
+
QUOTED_FLOAT = /\A\s*[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\z/
|
|
25
|
+
NONFINITE = {"NaN" => Float::NAN, "Infinity" => Float::INFINITY, "-Infinity" => -Float::INFINITY}.freeze
|
|
26
|
+
BASE64 = %r{\A[A-Za-z0-9+/\-_]*={0,2}\z}
|
|
27
|
+
TIMESTAMP = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:(Z)|([+-])(\d{2}):(\d{2}))\z/
|
|
28
|
+
DURATION = /\A(-)?(\d*)(?:\.(\d*))?s\z/
|
|
29
|
+
# A string literal, or -0 standing alone as a number
|
|
30
|
+
NEGATIVE_ZERO = /"(?:[^"\\]|\\.)*"|(?<![\w.])-0(?![\w.])/m
|
|
31
|
+
VALUE = "google.protobuf.Value"
|
|
32
|
+
NULL_VALUE = Encoder::NULL_VALUE
|
|
33
|
+
|
|
34
|
+
# @param ignore_unknown_fields [Boolean] skip unknown field names and enum names
|
|
35
|
+
# @param registry [Types::Registry] where the types inside Any are looked up
|
|
36
|
+
def initialize(ignore_unknown_fields: false, registry: nil)
|
|
37
|
+
@ignore_unknown = ignore_unknown_fields
|
|
38
|
+
@registry = registry
|
|
39
|
+
@path = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param klass [Class] a message class
|
|
43
|
+
# @param string [String] JSON
|
|
44
|
+
# @return [Message]
|
|
45
|
+
# @raise [ParseError]
|
|
46
|
+
def decode(klass, string)
|
|
47
|
+
tree = begin
|
|
48
|
+
# the stdlib reads -0 as the Integer 0, losing the sign a double field keeps
|
|
49
|
+
string = string.gsub(NEGATIVE_ZERO) { |m| (m == "-0") ? "-0.0" : m } if string.include?("-0")
|
|
50
|
+
::JSON.parse(string, allow_duplicate_key: false, allow_comments: false, allow_nan: false)
|
|
51
|
+
rescue ::JSON::ParserError, EncodingError, ArgumentError => e
|
|
52
|
+
raise ParseError, "invalid JSON: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
if tree.nil? && klass.descriptor.full_name != VALUE
|
|
55
|
+
raise ParseError, "expected a JSON object for #{klass.descriptor.full_name}, got null"
|
|
56
|
+
end
|
|
57
|
+
message(klass, tree)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def fail!(message)
|
|
63
|
+
raise ParseError.new(message, Path.to_s(@path))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def describe(node)
|
|
67
|
+
case node
|
|
68
|
+
when Hash then "an object"
|
|
69
|
+
when Array then "an array"
|
|
70
|
+
when nil then "null"
|
|
71
|
+
else node.inspect
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def message(klass, node)
|
|
76
|
+
special = SPECIAL[klass.descriptor.full_name]
|
|
77
|
+
return send(special, klass, node) if special
|
|
78
|
+
fail!("expected an object for #{klass.descriptor.full_name}, got #{describe(node)}") unless node.is_a?(Hash)
|
|
79
|
+
msg = klass.new
|
|
80
|
+
fields(msg, node)
|
|
81
|
+
msg
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fields(msg, node)
|
|
85
|
+
klass = msg.class
|
|
86
|
+
descriptor = klass.descriptor
|
|
87
|
+
oneofs = nil
|
|
88
|
+
seen = {}
|
|
89
|
+
extensions = nil
|
|
90
|
+
node.each do |key, value|
|
|
91
|
+
if key.start_with?("[") && key.end_with?("]")
|
|
92
|
+
extension(msg, key[1..-2], value, extensions ||= {})
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
field = descriptor.fields_by_json_name[key] || descriptor.fields_by_name[key]
|
|
96
|
+
unless field
|
|
97
|
+
next if @ignore_unknown
|
|
98
|
+
fail!("#{descriptor.full_name} has no field #{key.inspect}")
|
|
99
|
+
end
|
|
100
|
+
info = klass.__by_number.fetch(field.number)
|
|
101
|
+
@path << info.name
|
|
102
|
+
fail!("field #{info.name} is given more than once") if seen[info.number]
|
|
103
|
+
seen[info.number] = true
|
|
104
|
+
if value.nil? && (info.repeated || info.map || !null_is_value?(info))
|
|
105
|
+
@path.pop
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
if (oneof = field.real_oneof)
|
|
109
|
+
oneofs ||= {}
|
|
110
|
+
fail!("more than one field of oneof #{oneof.name} is set") if oneofs[oneof]
|
|
111
|
+
end
|
|
112
|
+
parsed = field_value(info, value)
|
|
113
|
+
unless parsed.equal?(SKIP)
|
|
114
|
+
oneofs[oneof] = true if oneof
|
|
115
|
+
begin
|
|
116
|
+
msg.__set(info.index, parsed)
|
|
117
|
+
rescue Protostar::TypeError => e
|
|
118
|
+
fail!(e.message)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
@path.pop
|
|
122
|
+
end
|
|
123
|
+
add_extensions(msg, extensions) if extensions
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# An extension given as "[full.name]": its values are collected, then
|
|
127
|
+
# kept where the binary codec keeps them, in the unknown fields
|
|
128
|
+
def extension(msg, name, value, collected)
|
|
129
|
+
@extensions ||= Text::Extensions.new(@registry, nil)
|
|
130
|
+
field = @extensions.named(msg.class.descriptor, name)
|
|
131
|
+
info = field && @extensions.info(field)
|
|
132
|
+
unless info
|
|
133
|
+
return if @ignore_unknown
|
|
134
|
+
fail!("#{msg.class.descriptor.full_name} has no extension #{name.inspect}")
|
|
135
|
+
end
|
|
136
|
+
fail!("extension #{name} is given more than once") if collected.key?(info)
|
|
137
|
+
@path << "[#{name}]"
|
|
138
|
+
if !value.nil? || (!info.repeated && null_is_value?(info))
|
|
139
|
+
parsed = field_value(info, value)
|
|
140
|
+
collected[info] = info.repeated ? parsed : [parsed] unless parsed.equal?(SKIP)
|
|
141
|
+
end
|
|
142
|
+
@path.pop
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def add_extensions(msg, collected)
|
|
146
|
+
collected.sort_by { |info, _| info.number }.each do |info, values|
|
|
147
|
+
@extensions.write(info, values).each { |u| msg.__add_unknown(u) }
|
|
148
|
+
end
|
|
149
|
+
msg.__seal_unknown
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# null is the null of Value and NullValue fields, and absence everywhere else
|
|
153
|
+
def null_is_value?(info)
|
|
154
|
+
if info.kind == F::ENUM
|
|
155
|
+
info.enum_module.descriptor.full_name == NULL_VALUE
|
|
156
|
+
elsif info.message?
|
|
157
|
+
info.message_class.descriptor.full_name == VALUE
|
|
158
|
+
else
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def field_value(info, value)
|
|
164
|
+
if info.map
|
|
165
|
+
fail!("expected an object for a map, got #{describe(value)}") unless value.is_a?(Hash)
|
|
166
|
+
out = {}
|
|
167
|
+
value.each do |k, v|
|
|
168
|
+
@path << Path::MapKey.new(k)
|
|
169
|
+
key = map_key(info.key, k)
|
|
170
|
+
parsed = element(info.value, v)
|
|
171
|
+
out[key] = parsed unless parsed.equal?(SKIP)
|
|
172
|
+
@path.pop
|
|
173
|
+
end
|
|
174
|
+
out
|
|
175
|
+
elsif info.repeated
|
|
176
|
+
fail!("expected an array, got #{describe(value)}") unless value.is_a?(Array)
|
|
177
|
+
out = []
|
|
178
|
+
value.each_with_index do |v, i|
|
|
179
|
+
@path << i
|
|
180
|
+
parsed = element(info, v)
|
|
181
|
+
out << parsed unless parsed.equal?(SKIP)
|
|
182
|
+
@path.pop
|
|
183
|
+
end
|
|
184
|
+
out
|
|
185
|
+
else
|
|
186
|
+
single(info, value)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# An element of a repeated field or a map value, where null is not absence
|
|
191
|
+
def element(info, value)
|
|
192
|
+
fail!("null is not allowed here") if value.nil? && !null_is_value?(info)
|
|
193
|
+
single(info, value)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def map_key(info, key)
|
|
197
|
+
case info.kind
|
|
198
|
+
when F::STRING then key
|
|
199
|
+
when F::BOOL
|
|
200
|
+
case key
|
|
201
|
+
when "true" then true
|
|
202
|
+
when "false" then false
|
|
203
|
+
else fail!("map key #{key.inspect} is not true or false")
|
|
204
|
+
end
|
|
205
|
+
else integer(info, key)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def single(info, value)
|
|
210
|
+
case info.kind
|
|
211
|
+
when F::DOUBLE, F::FLOAT then float(info, value)
|
|
212
|
+
when F::BOOL
|
|
213
|
+
fail!("expected true or false, got #{describe(value)}") unless value == true || value == false
|
|
214
|
+
value
|
|
215
|
+
when F::STRING
|
|
216
|
+
fail!("expected a string, got #{describe(value)}") unless value.is_a?(String)
|
|
217
|
+
fail!("string is not valid UTF-8") unless value.valid_encoding?
|
|
218
|
+
value
|
|
219
|
+
when F::BYTES then bytes(value)
|
|
220
|
+
when F::ENUM then enum(info, value)
|
|
221
|
+
when F::MESSAGE, F::GROUP then message(info.message_class, value)
|
|
222
|
+
else integer(info, value)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def integer(info, value)
|
|
227
|
+
value = Float(value) if value.is_a?(String) && !QUOTED_INTEGER.match?(value) && QUOTED_INTEGRAL.match?(value)
|
|
228
|
+
n = case value
|
|
229
|
+
when Integer, Float
|
|
230
|
+
f = value.to_f
|
|
231
|
+
limit = (info.kind == F::UINT64 || info.kind == F::FIXED64) ? UINT64_LIMIT : INT64_LIMIT
|
|
232
|
+
fail!("number #{value} is out of range") unless f.finite? && f < limit && f >= ((limit == INT64_LIMIT) ? -limit : 0)
|
|
233
|
+
fail!("number #{value} is not an integer") unless f == f.truncate
|
|
234
|
+
f.to_i
|
|
235
|
+
when String
|
|
236
|
+
fail!("#{value.inspect} is not an integer") unless QUOTED_INTEGER.match?(value)
|
|
237
|
+
Integer(value, 10)
|
|
238
|
+
else
|
|
239
|
+
fail!("expected an integer, got #{describe(value)}")
|
|
240
|
+
end
|
|
241
|
+
range = RANGES.fetch((info.kind == F::ENUM) ? F::INT32 : info.kind)
|
|
242
|
+
fail!("#{n} is out of range for #{info.descriptor.type}") unless range.cover?(n)
|
|
243
|
+
n
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def float(info, value)
|
|
247
|
+
f = case value
|
|
248
|
+
when Integer, Float then value.to_f
|
|
249
|
+
when String
|
|
250
|
+
NONFINITE.fetch(value) do
|
|
251
|
+
fail!("#{value.inspect} is not a number") unless QUOTED_FLOAT.match?(value)
|
|
252
|
+
Float(value.strip).tap { |x| fail!("#{value} is out of range") unless x.finite? }
|
|
253
|
+
end
|
|
254
|
+
else fail!("expected a number, got #{describe(value)}")
|
|
255
|
+
end
|
|
256
|
+
fail!("#{value} is out of range") if value.is_a?(Numeric) && !f.finite?
|
|
257
|
+
if info.kind == F::FLOAT
|
|
258
|
+
single = Message::Coercion.float32(f)
|
|
259
|
+
fail!("#{value} is out of range for float") if f.finite? && !single.finite?
|
|
260
|
+
single
|
|
261
|
+
else
|
|
262
|
+
f
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Standard or URL-safe base64, padded or not
|
|
267
|
+
def bytes(value)
|
|
268
|
+
fail!("expected a base64 string, got #{describe(value)}") unless value.is_a?(String)
|
|
269
|
+
size = value.bytesize
|
|
270
|
+
padded = value.end_with?("=")
|
|
271
|
+
unless BASE64.match?(value) && (padded ? size % 4 == 0 : size % 4 != 1)
|
|
272
|
+
fail!("#{value.inspect} is not base64")
|
|
273
|
+
end
|
|
274
|
+
s = value.tr("-_", "+/")
|
|
275
|
+
s = s.delete_suffix("=").delete_suffix("=")
|
|
276
|
+
s << "=" * ((4 - s.bytesize % 4) % 4)
|
|
277
|
+
s.unpack1("m").force_encoding(Encoding::BINARY)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def enum(info, value)
|
|
281
|
+
if value.nil?
|
|
282
|
+
0
|
|
283
|
+
elsif value.is_a?(String)
|
|
284
|
+
number = info.enum_numbers[value.to_sym]
|
|
285
|
+
return number if number
|
|
286
|
+
return SKIP if @ignore_unknown
|
|
287
|
+
fail!("#{info.enum_module.descriptor.full_name} has no value #{value.inspect}")
|
|
288
|
+
elsif value.is_a?(Integer) || value.is_a?(Float)
|
|
289
|
+
n = integer(info, value)
|
|
290
|
+
if info.closed_enum && !info.enum_names.key?(n)
|
|
291
|
+
return SKIP if @ignore_unknown
|
|
292
|
+
fail!("#{info.enum_module.descriptor.full_name} has no value #{n}")
|
|
293
|
+
end
|
|
294
|
+
n
|
|
295
|
+
else
|
|
296
|
+
fail!("expected an enum name or number, got #{describe(value)}")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def wrapper(klass, node)
|
|
301
|
+
info = klass.__by_name.fetch("value")
|
|
302
|
+
@path << "value"
|
|
303
|
+
v = single(info, node)
|
|
304
|
+
@path.pop
|
|
305
|
+
msg = klass.new
|
|
306
|
+
msg.__set(info.index, v)
|
|
307
|
+
msg
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def timestamp(klass, node)
|
|
311
|
+
fail!("expected an RFC 3339 timestamp string, got #{describe(node)}") unless node.is_a?(String)
|
|
312
|
+
m = TIMESTAMP.match(node) or fail!("#{node.inspect} is not an RFC 3339 timestamp")
|
|
313
|
+
fail!("#{node.inspect} has more than 9 fractional digits") if m[7] && m[7].size > 9
|
|
314
|
+
days = days_from_civil(m[1].to_i, m[2].to_i, m[3].to_i)
|
|
315
|
+
seconds = days * 86_400 + m[4].to_i * 3600 + m[5].to_i * 60 + m[6].to_i
|
|
316
|
+
unless m[8]
|
|
317
|
+
offset = m[10].to_i * 3600 + m[11].to_i * 60
|
|
318
|
+
seconds += (m[9] == "-") ? offset : -offset
|
|
319
|
+
end
|
|
320
|
+
unless (Encoder::TIMESTAMP_MIN..Encoder::TIMESTAMP_MAX).cover?(seconds)
|
|
321
|
+
fail!("#{node.inspect} is out of the Timestamp range")
|
|
322
|
+
end
|
|
323
|
+
klass.new(seconds: seconds, nanos: nanos(m[7]))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Days since 1970-01-01 of a proleptic Gregorian date; like upb, it
|
|
327
|
+
# does not check that the day exists in the month
|
|
328
|
+
def days_from_civil(y, m, d)
|
|
329
|
+
y -= 1 if m <= 2
|
|
330
|
+
era = y.div(400)
|
|
331
|
+
yoe = y - era * 400
|
|
332
|
+
doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2) / 5 + d - 1
|
|
333
|
+
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
|
|
334
|
+
era * 146_097 + doe - 719_468
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def nanos(digits)
|
|
338
|
+
return 0 if digits.nil? || digits.empty?
|
|
339
|
+
digits.ljust(9, "0").to_i
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def duration(klass, node)
|
|
343
|
+
fail!("expected a duration string, got #{describe(node)}") unless node.is_a?(String)
|
|
344
|
+
m = DURATION.match(node)
|
|
345
|
+
fail!("#{node.inspect} is not a duration") if m.nil? || (m[2].empty? && m[3].to_s.empty?)
|
|
346
|
+
fail!("#{node.inspect} has more than 9 fractional digits") if m[3] && m[3].size > 9
|
|
347
|
+
seconds = m[2].empty? ? 0 : m[2].to_i
|
|
348
|
+
fail!("#{node.inspect} is out of the Duration range") if seconds > Encoder::DURATION_MAX
|
|
349
|
+
nanos = nanos(m[3])
|
|
350
|
+
if m[1]
|
|
351
|
+
seconds = -seconds
|
|
352
|
+
nanos = -nanos
|
|
353
|
+
end
|
|
354
|
+
klass.new(seconds: seconds, nanos: nanos)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def field_mask(klass, node)
|
|
358
|
+
fail!("expected a field mask string, got #{describe(node)}") unless node.is_a?(String)
|
|
359
|
+
paths = node.empty? ? [] : node.split(",", -1)
|
|
360
|
+
paths.map! do |p|
|
|
361
|
+
fail!("field mask path #{p.inspect} may not contain '_'") if p.include?("_")
|
|
362
|
+
p.gsub(/[A-Z]/) { "_#{$&.downcase}" }
|
|
363
|
+
end
|
|
364
|
+
klass.new(paths: paths)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def struct(klass, node)
|
|
368
|
+
fail!("expected an object for google.protobuf.Struct, got #{describe(node)}") unless node.is_a?(Hash)
|
|
369
|
+
value_class = klass.__by_name.fetch("fields").value.message_class
|
|
370
|
+
fields = {}
|
|
371
|
+
node.each do |k, v|
|
|
372
|
+
@path << Path::MapKey.new(k)
|
|
373
|
+
fields[k] = value(value_class, v)
|
|
374
|
+
@path.pop
|
|
375
|
+
end
|
|
376
|
+
klass.new(fields: fields)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def list_value(klass, node)
|
|
380
|
+
fail!("expected an array for google.protobuf.ListValue, got #{describe(node)}") unless node.is_a?(Array)
|
|
381
|
+
value_class = klass.__by_name.fetch("values").message_class
|
|
382
|
+
values = node.each_with_index.map do |v, i|
|
|
383
|
+
@path << i
|
|
384
|
+
parsed = value(value_class, v)
|
|
385
|
+
@path.pop
|
|
386
|
+
parsed
|
|
387
|
+
end
|
|
388
|
+
klass.new(values: values)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def value(klass, node)
|
|
392
|
+
case node
|
|
393
|
+
when nil then klass.new(null_value: 0)
|
|
394
|
+
when true, false then klass.new(bool_value: node)
|
|
395
|
+
when String then klass.new(string_value: node)
|
|
396
|
+
when Integer, Float
|
|
397
|
+
f = node.to_f
|
|
398
|
+
fail!("#{node} is out of range") unless f.finite?
|
|
399
|
+
klass.new(number_value: f)
|
|
400
|
+
when Hash then klass.new(struct_value: struct(klass.__by_name.fetch("struct_value").message_class, node))
|
|
401
|
+
when Array then klass.new(list_value: list_value(klass.__by_name.fetch("list_value").message_class, node))
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def any(klass, node)
|
|
406
|
+
fail!("expected an object for google.protobuf.Any, got #{describe(node)}") unless node.is_a?(Hash)
|
|
407
|
+
return klass.new if node.empty?
|
|
408
|
+
url = node["@type"]
|
|
409
|
+
fail!("Any has no \"@type\"") if url.nil?
|
|
410
|
+
fail!("\"@type\" of Any must be a string") unless url.is_a?(String)
|
|
411
|
+
slash = url.rindex("/")
|
|
412
|
+
fail!("type URL #{url.inspect} needs a '/' and a non-empty host") if slash.nil? || slash == 0
|
|
413
|
+
name = url[(slash + 1)..]
|
|
414
|
+
inner_class = Types.find(name, registry: @registry || Types.registry)
|
|
415
|
+
fail!("cannot find the type #{name.inspect} of an Any") unless inner_class.is_a?(Class)
|
|
416
|
+
rest = node.except("@type")
|
|
417
|
+
inner = if SPECIAL.key?(inner_class.descriptor.full_name)
|
|
418
|
+
rest.each_key do |k|
|
|
419
|
+
next if k == "value" || @ignore_unknown
|
|
420
|
+
fail!("Any holding a #{inner_class.descriptor.full_name} has no field #{k.inspect}")
|
|
421
|
+
end
|
|
422
|
+
rest.key?("value") ? element_message(inner_class, rest["value"]) : inner_class.new
|
|
423
|
+
else
|
|
424
|
+
message(inner_class, rest)
|
|
425
|
+
end
|
|
426
|
+
klass.new(type_url: url, value: inner.encode)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def element_message(klass, node)
|
|
430
|
+
@path << "value"
|
|
431
|
+
msg = message(klass, node)
|
|
432
|
+
@path.pop
|
|
433
|
+
msg
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module JSON
|
|
5
|
+
# Where the encoder or parser is in a message, kept as a stack of field
|
|
6
|
+
# names, Integer indexes and map keys, turned into text only for errors
|
|
7
|
+
# @api private
|
|
8
|
+
module Path
|
|
9
|
+
# A map key on the stack, printed as `[key]`
|
|
10
|
+
class MapKey
|
|
11
|
+
# @return [Object]
|
|
12
|
+
attr_reader :key
|
|
13
|
+
|
|
14
|
+
def initialize(key)
|
|
15
|
+
@key = key
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# @param stack [Array<String, Integer, MapKey>]
|
|
23
|
+
# @return [String] e.g. `friends[2].name` or `labels["x"]`
|
|
24
|
+
def to_s(stack)
|
|
25
|
+
out = +""
|
|
26
|
+
stack.each do |seg|
|
|
27
|
+
case seg
|
|
28
|
+
when Integer then out << "[#{seg}]"
|
|
29
|
+
when MapKey then out << "[#{seg.key.inspect}]"
|
|
30
|
+
else
|
|
31
|
+
out << "." unless out.empty?
|
|
32
|
+
out << seg
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
out
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
# The proto3 JSON mapping, as google-protobuf's Ruby gem implements it.
|
|
5
|
+
#
|
|
6
|
+
# json = Protostar::JSON.encode(person) # also person.to_json
|
|
7
|
+
# Protostar::JSON.decode(Person, json) == person # also Person.decode_json(json)
|
|
8
|
+
#
|
|
9
|
+
# Well-known types take their special forms. The type inside an Any is
|
|
10
|
+
# looked up by its full name with {Types.find} in the given registry.
|
|
11
|
+
module JSON
|
|
12
|
+
class << self
|
|
13
|
+
# @param message [Message]
|
|
14
|
+
# @option options [Boolean] :preserve_proto_field_names field names as written in the
|
|
15
|
+
# .proto rather than their lowerCamel json_name; `preserve_proto_fieldnames`,
|
|
16
|
+
# google-protobuf's spelling, is accepted too
|
|
17
|
+
# @option options [Boolean] :emit_defaults also write fields without presence that
|
|
18
|
+
# hold their default, and empty repeated fields and maps; unset message
|
|
19
|
+
# fields and oneof members are still left out
|
|
20
|
+
# @option options [Boolean] :format_enums_as_integers enum numbers instead of names
|
|
21
|
+
# @option options [Types::Registry] :registry where the types inside Any are found
|
|
22
|
+
# @return [String] compact UTF-8 JSON
|
|
23
|
+
# @raise [EncodeError] for contents with no JSON form, like a Timestamp out of range
|
|
24
|
+
def encode(message, **options)
|
|
25
|
+
Encoder.new(**options).encode(message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param klass [Class] a message class
|
|
29
|
+
# @param json [String]
|
|
30
|
+
# @option options [Boolean] :ignore_unknown_fields skip names that are not fields
|
|
31
|
+
# and enum names that are not values, instead of raising
|
|
32
|
+
# @option options [Types::Registry] :registry where the types inside Any are found
|
|
33
|
+
# @return [Message]
|
|
34
|
+
# @raise [ParseError] with the path of the offending field
|
|
35
|
+
def decode(klass, json, **options)
|
|
36
|
+
Parser.new(**options).decode(klass, json)
|
|
37
|
+
rescue Protostar::TypeError => e
|
|
38
|
+
raise ParseError, e.message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Lays compact JSON out as the stdlib generator would with `state`
|
|
42
|
+
# (indent, space, newlines, current depth). Tokens are copied as they
|
|
43
|
+
# are, so numbers keep the spelling the encoder gave them.
|
|
44
|
+
# @param json [String] compact JSON, as {encode} writes it
|
|
45
|
+
# @param state [::JSON::State]
|
|
46
|
+
# @return [String]
|
|
47
|
+
# @api private
|
|
48
|
+
def layout(json, state)
|
|
49
|
+
indent = state.indent
|
|
50
|
+
object_nl = state.object_nl
|
|
51
|
+
array_nl = state.array_nl
|
|
52
|
+
colon = "#{state.space_before}:#{state.space}"
|
|
53
|
+
return json if indent.empty? && object_nl.empty? && array_nl.empty? && colon == ":"
|
|
54
|
+
depth = state.depth
|
|
55
|
+
open = []
|
|
56
|
+
out = +""
|
|
57
|
+
tokens = json.scan(LAYOUT_TOKENS)
|
|
58
|
+
i = 0
|
|
59
|
+
while i < tokens.size
|
|
60
|
+
t = tokens[i]
|
|
61
|
+
case t
|
|
62
|
+
when "{", "["
|
|
63
|
+
close = (t == "{") ? "}" : "]"
|
|
64
|
+
if tokens[i + 1] == close
|
|
65
|
+
out << t << close
|
|
66
|
+
i += 1
|
|
67
|
+
else
|
|
68
|
+
nl = (t == "{") ? object_nl : array_nl
|
|
69
|
+
open << nl
|
|
70
|
+
depth += 1
|
|
71
|
+
out << t << nl << indent * depth
|
|
72
|
+
end
|
|
73
|
+
when "}", "]"
|
|
74
|
+
depth -= 1
|
|
75
|
+
nl = open.pop
|
|
76
|
+
out << nl << indent * depth unless nl.empty?
|
|
77
|
+
out << t
|
|
78
|
+
when "," then out << "," << open.last << indent * depth
|
|
79
|
+
when ":" then out << colon
|
|
80
|
+
else out << t
|
|
81
|
+
end
|
|
82
|
+
i += 1
|
|
83
|
+
end
|
|
84
|
+
out
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
LAYOUT_TOKENS = /"(?:[^"\\]|\\.)*"|[{}\[\],:]|[^{}\[\],:"]+/m
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
require_relative "json/errors"
|
|
93
|
+
require_relative "json/path"
|
|
94
|
+
require_relative "json/encoder"
|
|
95
|
+
require_relative "json/parser"
|