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.
Files changed (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. 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"