Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
davebenvenuti committed Jan 29, 2025
1 parent 99ad480 commit 8cd3cca
Show file tree
Hide file tree
Showing 21 changed files with 1,851 additions and 279 deletions.
135 changes: 107 additions & 28 deletions lib/protoboeuf/codegen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,105 @@ def resolve
end
end

# Generates #to_h, #as_json, and #to_json methods
class HashSerializationCompiler
include TypeHelper

attr_reader :message, :fields, :oneof_selection_fields, :generate_types

class << self
def result(message:, fields:, oneof_selection_fields:, generate_types:)
new(message:, fields:, oneof_selection_fields:, generate_types:).result
end
end

def initialize(message:, fields:, oneof_selection_fields:, generate_types:)
@message = message
@fields = fields.sort_by(&:number) # Serialize fields in their proto order
@oneof_selection_fields = oneof_selection_fields
@generate_types = generate_types
end

def result
<<~RUBY
#{type_signature(returns: "T::Hash[Symbol, T.untyped]")}
#{hash_serializer_rb(method_name: "to_h")}
#{type_signature(params: { options: "T::Hash[T.untyped, T.untyped]" }, returns: "T::Hash[Symbol, T.untyped]")}
#{hash_serializer_rb(method_name: "as_json", json: true)}
def to_json(options = {})
require 'json'
JSON.dump(as_json(options))
end
RUBY
end

private

def hash_serializer_rb(method_name:, json: false)
<<~HASH_SERIALIZER
def #{method_name}(options = {})
result = {}
#{fields.map { |f| assign_result_hash_rb(f, json:) }.join("\n")}
result
end
HASH_SERIALIZER
end

def assign_result_hash_rb(field, json: false)
key = hash_key_rb(field, json:)
value = hash_value_rb(field, json:)

if field.has_oneof_index? && !field.optional?
oneof_selection_field_name = oneof_selection_fields[field.oneof_index].name.dump
field_name = field.name.dump

"result[#{key}] = #{value} if send(:#{oneof_selection_field_name}) == :#{field_name}"
elsif field.repeated?
"#{value}.tap { |v| result[#{key}] = v if !options[:compact] || v.any? }"
elsif field.optional?
"result[#{key}] = #{value} if !options[:compact] || has_#{field.name}?"
else
"result[#{key}] = #{value}"
end
end

def hash_key_rb(field, json:)
json ? field.json_name.dump : ":#{field.name.dump}"
end

def hash_value_rb(field, json:)
return hash_value_for_map_rb(field, json:) if field.map_field?

# For primitives or arrays of primitives we can just use the instance variable value
return field.iv_name unless field.type == :TYPE_MESSAGE

recurse_with = json ? "as_json(options)" : "to_h(options)"

if field.repeated?
# repeated maps aren't possible so we don't have to worry about to_h arity or as_json not being defined
"#{field.iv_name}.map { |v| v.#{recurse_with} }"
else
"#{field.iv_name}.#{recurse_with}"
end
end

def hash_value_for_map_rb(field, json:)
if field.map_type.value.type == :TYPE_MESSAGE
recurse_with = json ? "as_json(options)" : "to_h(options)"

<<~RUBY
#{field.iv_name}.transform_values { |value| value.#{recurse_with} }
RUBY
else
field.iv_name
end
end
end

class MessageCompiler
attr_reader :generate_types, :requires

Expand All @@ -88,8 +187,13 @@ def result(message, toplevel_enums, generate_types:, requires:, syntax:, options
end
end

attr_reader :message, :fields, :oneof_fields, :syntax
attr_reader :optional_fields, :enum_field_types
attr_reader :message,
:fields,
:oneof_fields,
:syntax,
:optional_fields,
:enum_field_types,
:oneof_selection_fields

def initialize(message, toplevel_enums, generate_types:, requires:, syntax:, options:)
@message = message
Expand Down Expand Up @@ -160,32 +264,7 @@ def class_body
end

def conversion
fields = self.fields.reject do |field|
field.has_oneof_index? && !field.optional?
end

oneofs = @oneof_selection_fields.map do |field|
"send(#{field.name.dump}).tap { |f| result[f.to_sym] = send(f) if f }"
end

<<~RUBY
#{type_signature(returns: "T::Hash[Symbol, T.untyped]")}
def to_h
result = {}
#{(oneofs + fields.map { |field| convert_field(field) }).join("\n")}
result
end
RUBY
end

def convert_field(field)
if field.repeated?
"result['#{field.name}'.to_sym] = #{field.iv_name}"
elsif field.type == :TYPE_MESSAGE
"result['#{field.name}'.to_sym] = #{field.iv_name}.to_h"
else
"result['#{field.name}'.to_sym] = #{field.iv_name}"
end
HashSerializationCompiler.result(message:, fields:, oneof_selection_fields:, generate_types:)
end

def encode
Expand Down
11 changes: 10 additions & 1 deletion lib/protoboeuf/decorated_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ class DecoratedField

extend Forwardable

def_delegators :@original_field, :name, :label, :type_name, :type, :number, :options, :oneof_index, :has_oneof_index?
def_delegators :@original_field,
:name,
:label,
:type_name,
:type,
:number,
:options,
:oneof_index,
:has_oneof_index?,
:json_name

def initialize(field:, message:, syntax:)
@original_field = field
Expand Down
22 changes: 19 additions & 3 deletions lib/protoboeuf/google/protobuf/any.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions lib/protoboeuf/google/protobuf/boolvalue.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions lib/protoboeuf/google/protobuf/bytesvalue.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8cd3cca

Please sign in to comment.