From 5b3533dd0b9e779baf88f955d5699e94dd798a93 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Sun, 20 Oct 2024 18:51:24 +0000 Subject: [PATCH 1/7] Re-write in Ruby --- benchmarks/type_casts/bm_panko.rb | 52 ++- .../attributes_writer/active_record.c | 208 --------- .../attributes_writer/active_record.h | 17 - .../attributes_writer/attributes_writer.c | 55 --- .../attributes_writer/attributes_writer.h | 35 -- .../attributes_writer/common.c | 10 - .../attributes_writer/common.h | 9 - ext/panko_serializer/attributes_writer/hash.c | 13 - ext/panko_serializer/attributes_writer/hash.h | 7 - .../attributes_writer/plain.c | 13 - .../attributes_writer/plain.h | 7 - .../attributes_writer/type_cast/type_cast.c | 398 ------------------ .../attributes_writer/type_cast/type_cast.h | 81 ---- ext/panko_serializer/common.h | 9 - ext/panko_serializer/panko_serializer.c | 179 +------- ext/panko_serializer/panko_serializer.h | 12 - .../serialization_descriptor/association.c | 90 ---- .../serialization_descriptor/association.h | 20 - .../serialization_descriptor/attribute.c | 96 ----- .../serialization_descriptor/attribute.h | 27 -- .../serialization_descriptor.c | 177 -------- .../serialization_descriptor.h | 30 -- .../type_cast => }/time_conversion.c | 0 .../type_cast => }/time_conversion.h | 0 lib/panko/array_serializer.rb | 3 +- lib/panko/association.rb | 13 + lib/panko/attribute.rb | 54 ++- .../active_record/context.rb | 80 ++++ .../values_writer/boolean_writer.rb | 35 ++ .../values_writer/datetime_writer.rb | 22 + .../values_writer/float_writer.rb | 17 + .../values_writer/integer_writer.rb | 29 ++ .../values_writer/json_writer.rb | 33 ++ .../values_writer/string_writer.rb | 21 + .../active_record/values_writer/writer.rb | 79 ++++ .../attributes_writer/active_record/writer.rb | 21 + lib/panko/impl/attributes_writer/creator.rb | 21 + .../impl/attributes_writer/hash_writer.rb | 12 + .../impl/attributes_writer/plain_writer.rb | 12 + lib/panko/impl/serializer.rb | 114 +++++ lib/panko/serialization_descriptor.rb | 29 +- lib/panko/serializer.rb | 5 +- lib/panko_serializer.rb | 7 + spec/panko/type_cast_spec.rb | 163 ++++--- 44 files changed, 749 insertions(+), 1566 deletions(-) delete mode 100644 ext/panko_serializer/attributes_writer/active_record.c delete mode 100644 ext/panko_serializer/attributes_writer/active_record.h delete mode 100644 ext/panko_serializer/attributes_writer/attributes_writer.c delete mode 100644 ext/panko_serializer/attributes_writer/attributes_writer.h delete mode 100644 ext/panko_serializer/attributes_writer/common.c delete mode 100644 ext/panko_serializer/attributes_writer/common.h delete mode 100644 ext/panko_serializer/attributes_writer/hash.c delete mode 100644 ext/panko_serializer/attributes_writer/hash.h delete mode 100644 ext/panko_serializer/attributes_writer/plain.c delete mode 100644 ext/panko_serializer/attributes_writer/plain.h delete mode 100644 ext/panko_serializer/attributes_writer/type_cast/type_cast.c delete mode 100644 ext/panko_serializer/attributes_writer/type_cast/type_cast.h delete mode 100644 ext/panko_serializer/common.h delete mode 100644 ext/panko_serializer/panko_serializer.h delete mode 100644 ext/panko_serializer/serialization_descriptor/association.c delete mode 100644 ext/panko_serializer/serialization_descriptor/association.h delete mode 100644 ext/panko_serializer/serialization_descriptor/attribute.c delete mode 100644 ext/panko_serializer/serialization_descriptor/attribute.h delete mode 100644 ext/panko_serializer/serialization_descriptor/serialization_descriptor.c delete mode 100644 ext/panko_serializer/serialization_descriptor/serialization_descriptor.h rename ext/panko_serializer/{attributes_writer/type_cast => }/time_conversion.c (100%) rename ext/panko_serializer/{attributes_writer/type_cast => }/time_conversion.h (100%) create mode 100644 lib/panko/impl/attributes_writer/active_record/context.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/boolean_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/float_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/integer_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/json_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/string_writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb create mode 100644 lib/panko/impl/attributes_writer/active_record/writer.rb create mode 100644 lib/panko/impl/attributes_writer/creator.rb create mode 100644 lib/panko/impl/attributes_writer/hash_writer.rb create mode 100644 lib/panko/impl/attributes_writer/plain_writer.rb create mode 100644 lib/panko/impl/serializer.rb diff --git a/benchmarks/type_casts/bm_panko.rb b/benchmarks/type_casts/bm_panko.rb index 06f9093f..1b210b33 100644 --- a/benchmarks/type_casts/bm_panko.rb +++ b/benchmarks/type_casts/bm_panko.rb @@ -2,16 +2,39 @@ require_relative "support" +class NoopWriter + attr_reader :value + def push_value(value, key) + @value = value + nil + end + + def push_json(value, key) + nil + end +end + +class Attribute + attr_reader :name_for_serialization, :type + + def initialize(name_for_serialization:, type:) + @name_for_serialization = name_for_serialization + @type = type + end +end + def panko_type_convert(type_klass, from, to) converter = type_klass.new - assert type_klass.name.to_s, Panko._type_cast(converter, from), to + + writer = NoopWriter.new + attribute = Attribute.new(name_for_serialization: "key", type: converter) Benchmark.run("#{type_klass.name}_TypeCast") do - Panko._type_cast(converter, from) + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, from) end Benchmark.run("#{type_klass.name}_NoTypeCast") do - Panko._type_cast(converter, to) + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, to) end end @@ -23,14 +46,19 @@ def utc_panko_time type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type) - to = Panko._type_cast(converter, from) + writer = NoopWriter.new + attribute = Attribute.new(name_for_serialization: "key", type: converter) + + # get the to value + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, from) + to = writer.value Benchmark.run("#{tz}_#{type.class.name}_TypeCast") do - Panko._type_cast(converter, from) + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, from) end Benchmark.run("#{tz}_#{type.class.name}_NoTypeCast") do - Panko._type_cast(converter, to) + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, to) end end @@ -40,18 +68,24 @@ def db_panko_time from = "2017-07-10 09:26:40.937392" + writer = NoopWriter.new + attribute = Attribute.new(name_for_serialization: "key", type: converter) + Benchmark.run("Panko_Time_TypeCast") do - Panko._type_cast(converter, from) + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, from) end end panko_type_convert ActiveRecord::Type::String, 1, "1" -panko_type_convert ActiveRecord::Type::Text, 1, "1" panko_type_convert ActiveRecord::Type::Integer, "1", 1 +panko_type_convert ActiveRecord::Type::Text, 1, "1" panko_type_convert ActiveRecord::Type::Float, "1.23", 1.23 panko_type_convert ActiveRecord::Type::Boolean, "true", true panko_type_convert ActiveRecord::Type::Boolean, "t", true +db_panko_time +utc_panko_time + if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json" panko_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json, '{"a":1}', '{"a":1}' end @@ -61,5 +95,3 @@ def db_panko_time if check_if_exists "ActiveRecord::Type::Json" panko_type_convert ActiveRecord::Type::Json, '{"a":1}', '{"a":1}' end -db_panko_time -utc_panko_time diff --git a/ext/panko_serializer/attributes_writer/active_record.c b/ext/panko_serializer/attributes_writer/active_record.c deleted file mode 100644 index 593e266e..00000000 --- a/ext/panko_serializer/attributes_writer/active_record.c +++ /dev/null @@ -1,208 +0,0 @@ -#include "active_record.h" - -static ID attributes_id; -static ID types_id; -static ID additional_types_id; -static ID values_id; -static ID delegate_hash_id; - -static ID value_before_type_cast_id; -static ID type_id; - -static ID fetch_id; - -// ActiveRecord::Result::IndexedRow -static VALUE ar_result_indexed_row = Qundef; -static int fetched_ar_result_indexed_row = 0; - -VALUE fetch_ar_result_indexed_row_type() { - if (fetched_ar_result_indexed_row == 1) { - return ar_result_indexed_row; - } - - fetched_ar_result_indexed_row = 1; - - VALUE ar, ar_result; - - ar = rb_const_get_at(rb_cObject, rb_intern("ActiveRecord")); - - // ActiveRecord::Result - ar_result = rb_const_get_at(ar, rb_intern("Result")); - - if (rb_const_defined_at(ar_result, rb_intern("IndexedRow")) == (int)Qtrue) { - ar_result_indexed_row = rb_const_get_at(ar_result, rb_intern("IndexedRow")); - } - - return ar_result_indexed_row; -} - -struct attributes { - // Hash - VALUE attributes_hash; - size_t attributes_hash_size; - - // Hash - VALUE types; - // Hash - VALUE additional_types; - // heuristics - bool tryToReadFromAdditionalTypes; - - // Rails <8: Hash - // Rails >=8: ActiveRecord::Result::IndexedRow - VALUE values; - - // Hash - VALUE indexed_row_column_indexes; - // Array or NIL - VALUE indexed_row_row; - bool is_indexed_row; -}; - -struct attributes init_context(VALUE obj) { - volatile VALUE attributes_set = rb_ivar_get(obj, attributes_id); - volatile VALUE attributes_hash = rb_ivar_get(attributes_set, attributes_id); - - struct attributes attrs = (struct attributes){ - .attributes_hash = - PANKO_EMPTY_HASH(attributes_hash) ? Qnil : attributes_hash, - .attributes_hash_size = 0, - - .types = rb_ivar_get(attributes_set, types_id), - .additional_types = rb_ivar_get(attributes_set, additional_types_id), - .tryToReadFromAdditionalTypes = - PANKO_EMPTY_HASH(rb_ivar_get(attributes_set, additional_types_id)) == - false, - - .values = rb_ivar_get(attributes_set, values_id), - .is_indexed_row = false, - .indexed_row_column_indexes = Qnil, - .indexed_row_row = Qnil, - }; - - if (attrs.attributes_hash != Qnil) { - attrs.attributes_hash_size = RHASH_SIZE(attrs.attributes_hash); - } - - if (CLASS_OF(attrs.values) == fetch_ar_result_indexed_row_type()) { - volatile VALUE indexed_row_column_indexes = - rb_ivar_get(attrs.values, rb_intern("@column_indexes")); - - volatile VALUE indexed_row_row = - rb_ivar_get(attrs.values, rb_intern("@row")); - - attrs.indexed_row_column_indexes = indexed_row_column_indexes; - attrs.indexed_row_row = indexed_row_row; - attrs.is_indexed_row = true; - } - - return attrs; -} - -VALUE _read_value_from_indexed_row(struct attributes attributes_ctx, - volatile VALUE member) { - volatile VALUE value = Qnil; - - if (NIL_P(attributes_ctx.indexed_row_column_indexes) || - NIL_P(attributes_ctx.indexed_row_row)) { - return value; - } - - volatile VALUE column_index = - rb_hash_aref(attributes_ctx.indexed_row_column_indexes, member); - - if (NIL_P(column_index)) { - return value; - } - - volatile VALUE row = attributes_ctx.indexed_row_row; - if (NIL_P(row)) { - return value; - } - - return RARRAY_AREF(row, NUM2INT(column_index)); -} - -VALUE read_attribute(struct attributes attributes_ctx, Attribute attribute, - volatile VALUE* isJson) { - volatile VALUE member, value; - - member = attribute->name_str; - value = Qnil; - - if ( - // we have attributes_hash - !NIL_P(attributes_ctx.attributes_hash) - // It's not empty - && (attributes_ctx.attributes_hash_size > 0)) { - volatile VALUE attribute_metadata = - rb_hash_aref(attributes_ctx.attributes_hash, member); - - if (attribute_metadata != Qnil) { - value = rb_ivar_get(attribute_metadata, value_before_type_cast_id); - - if (NIL_P(attribute->type)) { - attribute->type = rb_ivar_get(attribute_metadata, type_id); - } - } - } - - if (NIL_P(value) && !NIL_P(attributes_ctx.values)) { - if (attributes_ctx.is_indexed_row == true) { - value = _read_value_from_indexed_row(attributes_ctx, member); - } else { - value = rb_hash_aref(attributes_ctx.values, member); - } - } - - if (NIL_P(attribute->type) && !NIL_P(value)) { - if (attributes_ctx.tryToReadFromAdditionalTypes == true) { - attribute->type = rb_hash_aref(attributes_ctx.additional_types, member); - } - - if (!NIL_P(attributes_ctx.types) && NIL_P(attribute->type)) { - attribute->type = rb_hash_aref(attributes_ctx.types, member); - } - } - - if (!NIL_P(attribute->type) && !NIL_P(value)) { - return type_cast(attribute->type, value, isJson); - } - - return value; -} - -void active_record_attributes_writer(VALUE obj, VALUE attributes, - EachAttributeFunc write_value, - VALUE writer) { - long i; - struct attributes attributes_ctx = init_context(obj); - volatile VALUE record_class = CLASS_OF(obj); - - for (i = 0; i < RARRAY_LEN(attributes); i++) { - volatile VALUE raw_attribute = RARRAY_AREF(attributes, i); - Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute); - attribute_try_invalidate(attribute, record_class); - - volatile VALUE isJson = Qfalse; - volatile VALUE value = read_attribute(attributes_ctx, attribute, &isJson); - - write_value(writer, attr_name_for_serialization(attribute), value, isJson); - } -} - -void init_active_record_attributes_writer(VALUE mPanko) { - attributes_id = rb_intern("@attributes"); - delegate_hash_id = rb_intern("@delegate_hash"); - values_id = rb_intern("@values"); - types_id = rb_intern("@types"); - additional_types_id = rb_intern("@additional_types"); - type_id = rb_intern("@type"); - value_before_type_cast_id = rb_intern("@value_before_type_cast"); - fetch_id = rb_intern("fetch"); -} - -void panko_init_active_record(VALUE mPanko) { - init_active_record_attributes_writer(mPanko); - panko_init_type_cast(mPanko); -} \ No newline at end of file diff --git a/ext/panko_serializer/attributes_writer/active_record.h b/ext/panko_serializer/attributes_writer/active_record.h deleted file mode 100644 index ac213481..00000000 --- a/ext/panko_serializer/attributes_writer/active_record.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -#include "../common.h" -#include "common.h" -#include "serialization_descriptor/attribute.h" -#include "type_cast/type_cast.h" - -extern void active_record_attributes_writer(VALUE object, VALUE attributes, - EachAttributeFunc func, - VALUE writer); - -void init_active_record_attributes_writer(VALUE mPanko); - -void panko_init_active_record(VALUE mPanko); \ No newline at end of file diff --git a/ext/panko_serializer/attributes_writer/attributes_writer.c b/ext/panko_serializer/attributes_writer/attributes_writer.c deleted file mode 100644 index 709548f1..00000000 --- a/ext/panko_serializer/attributes_writer/attributes_writer.c +++ /dev/null @@ -1,55 +0,0 @@ -#include "attributes_writer.h" - -static bool types_initialized = false; -static VALUE ar_base_type = Qundef; - -VALUE init_types(VALUE v) { - if (types_initialized == true) { - return Qundef; - } - - types_initialized = true; - - volatile VALUE ar_type = - rb_const_get_at(rb_cObject, rb_intern("ActiveRecord")); - - ar_base_type = rb_const_get_at(ar_type, rb_intern("Base")); - rb_global_variable(&ar_base_type); - - return Qundef; -} - -AttributesWriter create_attributes_writer(VALUE object) { - // If ActiveRecord::Base can't be found it will throw error - int isErrored; - rb_protect(init_types, Qnil, &isErrored); - - if (ar_base_type != Qundef && - rb_obj_is_kind_of(object, ar_base_type) == Qtrue) { - return (AttributesWriter){ - .object_type = ActiveRecord, - .write_attributes = active_record_attributes_writer}; - } - - if (!RB_SPECIAL_CONST_P(object) && BUILTIN_TYPE(object) == T_HASH) { - return (AttributesWriter){.object_type = Hash, - .write_attributes = hash_attributes_writer}; - } - - return (AttributesWriter){.object_type = Plain, - .write_attributes = plain_attributes_writer}; - - return create_empty_attributes_writer(); -} - -void empty_write_attributes(VALUE obj, VALUE attributes, EachAttributeFunc func, - VALUE writer) {} - -AttributesWriter create_empty_attributes_writer() { - return (AttributesWriter){.object_type = UnknownObjectType, - .write_attributes = empty_write_attributes}; -} - -void init_attributes_writer(VALUE mPanko) { - init_active_record_attributes_writer(mPanko); -} diff --git a/ext/panko_serializer/attributes_writer/attributes_writer.h b/ext/panko_serializer/attributes_writer/attributes_writer.h deleted file mode 100644 index e6319b51..00000000 --- a/ext/panko_serializer/attributes_writer/attributes_writer.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include - -#include "active_record.h" -#include "common.h" -#include "hash.h" -#include "plain.h" - -enum ObjectType { - UnknownObjectType = 0, - ActiveRecord = 1, - Plain = 2, - Hash = 3 -}; - -typedef struct _AttributesWriter { - enum ObjectType object_type; - - void (*write_attributes)(VALUE object, VALUE attributes, - EachAttributeFunc func, VALUE context); -} AttributesWriter; - -/** - * Infers the attributes writer from the object type - */ -AttributesWriter create_attributes_writer(VALUE object); - -/** - * Creates empty writer - * Useful when the writer is not known, and you need init something - */ -AttributesWriter create_empty_attributes_writer(); - -void init_attributes_writer(VALUE mPanko); diff --git a/ext/panko_serializer/attributes_writer/common.c b/ext/panko_serializer/attributes_writer/common.c deleted file mode 100644 index 1906aea0..00000000 --- a/ext/panko_serializer/attributes_writer/common.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "common.h" - -VALUE attr_name_for_serialization(Attribute attribute) { - volatile VALUE name_str = attribute->name_str; - if (attribute->alias_name != Qnil) { - name_str = attribute->alias_name; - } - - return name_str; -} diff --git a/ext/panko_serializer/attributes_writer/common.h b/ext/panko_serializer/attributes_writer/common.h deleted file mode 100644 index 801054f1..00000000 --- a/ext/panko_serializer/attributes_writer/common.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "../serialization_descriptor/attribute.h" -#include "ruby.h" - -typedef void (*EachAttributeFunc)(VALUE writer, VALUE name, VALUE value, - VALUE isJson); - -VALUE attr_name_for_serialization(Attribute attribute); diff --git a/ext/panko_serializer/attributes_writer/hash.c b/ext/panko_serializer/attributes_writer/hash.c deleted file mode 100644 index a9e4851a..00000000 --- a/ext/panko_serializer/attributes_writer/hash.c +++ /dev/null @@ -1,13 +0,0 @@ -#include "hash.h" - -void hash_attributes_writer(VALUE obj, VALUE attributes, - EachAttributeFunc write_value, VALUE writer) { - long i; - for (i = 0; i < RARRAY_LEN(attributes); i++) { - volatile VALUE raw_attribute = RARRAY_AREF(attributes, i); - Attribute attribute = attribute_read(raw_attribute); - - write_value(writer, attr_name_for_serialization(attribute), - rb_hash_aref(obj, attribute->name_str), Qfalse); - } -} diff --git a/ext/panko_serializer/attributes_writer/hash.h b/ext/panko_serializer/attributes_writer/hash.h deleted file mode 100644 index f10cd9d7..00000000 --- a/ext/panko_serializer/attributes_writer/hash.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "common.h" -#include "ruby.h" - -void hash_attributes_writer(VALUE obj, VALUE attributes, EachAttributeFunc func, - VALUE writer); diff --git a/ext/panko_serializer/attributes_writer/plain.c b/ext/panko_serializer/attributes_writer/plain.c deleted file mode 100644 index 8fc60dce..00000000 --- a/ext/panko_serializer/attributes_writer/plain.c +++ /dev/null @@ -1,13 +0,0 @@ -#include "plain.h" - -void plain_attributes_writer(VALUE obj, VALUE attributes, - EachAttributeFunc write_value, VALUE writer) { - long i; - for (i = 0; i < RARRAY_LEN(attributes); i++) { - volatile VALUE raw_attribute = RARRAY_AREF(attributes, i); - Attribute attribute = attribute_read(raw_attribute); - - write_value(writer, attr_name_for_serialization(attribute), - rb_funcall(obj, attribute->name_id, 0), Qfalse); - } -} diff --git a/ext/panko_serializer/attributes_writer/plain.h b/ext/panko_serializer/attributes_writer/plain.h deleted file mode 100644 index a973b89d..00000000 --- a/ext/panko_serializer/attributes_writer/plain.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "common.h" -#include "ruby.h" - -void plain_attributes_writer(VALUE obj, VALUE attributes, - EachAttributeFunc func, VALUE writer); diff --git a/ext/panko_serializer/attributes_writer/type_cast/type_cast.c b/ext/panko_serializer/attributes_writer/type_cast/type_cast.c deleted file mode 100644 index 46db2194..00000000 --- a/ext/panko_serializer/attributes_writer/type_cast/type_cast.c +++ /dev/null @@ -1,398 +0,0 @@ -#include "type_cast.h" - -#include "time_conversion.h" - -ID deserialize_from_db_id = 0; -ID to_s_id = 0; -ID to_i_id = 0; - -static VALUE oj_type = Qundef; -static VALUE oj_parseerror_type = Qundef; -ID oj_sc_parse_id = 0; - -// Caching ActiveRecord Types -static VALUE ar_string_type = Qundef; -static VALUE ar_text_type = Qundef; -static VALUE ar_float_type = Qundef; -static VALUE ar_integer_type = Qundef; -static VALUE ar_boolean_type = Qundef; -static VALUE ar_date_time_type = Qundef; -static VALUE ar_time_zone_converter = Qundef; -static VALUE ar_json_type = Qundef; - -static VALUE ar_pg_integer_type = Qundef; -static VALUE ar_pg_float_type = Qundef; -static VALUE ar_pg_uuid_type = Qundef; -static VALUE ar_pg_json_type = Qundef; -static VALUE ar_pg_jsonb_type = Qundef; -static VALUE ar_pg_array_type = Qundef; -static VALUE ar_pg_date_time_type = Qundef; -static VALUE ar_pg_timestamp_type = Qundef; - -static int initiailized = 0; - -VALUE cache_postgres_type_lookup(VALUE ar) { - VALUE ar_connection_adapters, ar_postgresql, ar_oid; - - ar_connection_adapters = rb_const_get_at(ar, rb_intern("ConnectionAdapters")); - if (ar_connection_adapters == Qundef) { - return Qfalse; - } - - ar_postgresql = - rb_const_get_at(ar_connection_adapters, rb_intern("PostgreSQL")); - if (ar_postgresql == Qundef) { - return Qfalse; - } - - ar_oid = rb_const_get_at(ar_postgresql, rb_intern("OID")); - if (ar_oid == Qundef) { - return Qfalse; - } - - if (rb_const_defined_at(ar_oid, rb_intern("Float")) == (int)Qtrue) { - ar_pg_float_type = rb_const_get_at(ar_oid, rb_intern("Float")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("Integer")) == (int)Qtrue) { - ar_pg_integer_type = rb_const_get_at(ar_oid, rb_intern("Integer")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("Uuid")) == (int)Qtrue) { - ar_pg_uuid_type = rb_const_get_at(ar_oid, rb_intern("Uuid")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("Json")) == (int)Qtrue) { - ar_pg_json_type = rb_const_get_at(ar_oid, rb_intern("Json")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("Jsonb")) == (int)Qtrue) { - ar_pg_jsonb_type = rb_const_get_at(ar_oid, rb_intern("Jsonb")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("DateTime")) == (int)Qtrue) { - ar_pg_date_time_type = rb_const_get_at(ar_oid, rb_intern("DateTime")); - } - - if (rb_const_defined_at(ar_oid, rb_intern("Timestamp")) == (int)Qtrue) { - ar_pg_timestamp_type = rb_const_get_at(ar_oid, rb_intern("Timestamp")); - } - - return Qtrue; -} - -VALUE cache_time_zone_type_lookup(VALUE ar) { - VALUE ar_attr_methods, ar_time_zone_conversion; - - // ActiveRecord::AttributeMethods - ar_attr_methods = rb_const_get_at(ar, rb_intern("AttributeMethods")); - if (ar_attr_methods == Qundef) { - return Qfalse; - } - - // ActiveRecord::AttributeMethods::TimeZoneConversion - ar_time_zone_conversion = - rb_const_get_at(ar_attr_methods, rb_intern("TimeZoneConversion")); - if (ar_time_zone_conversion == Qundef) { - return Qfalse; - } - - ar_time_zone_converter = - rb_const_get_at(ar_time_zone_conversion, rb_intern("TimeZoneConverter")); - - return Qtrue; -} - -void cache_type_lookup() { - if (initiailized == 1) { - return; - } - - initiailized = 1; - - VALUE ar, ar_type, ar_type_methods; - - ar = rb_const_get_at(rb_cObject, rb_intern("ActiveRecord")); - - // ActiveRecord::Type - ar_type = rb_const_get_at(ar, rb_intern("Type")); - - ar_string_type = rb_const_get_at(ar_type, rb_intern("String")); - ar_text_type = rb_const_get_at(ar_type, rb_intern("Text")); - ar_float_type = rb_const_get_at(ar_type, rb_intern("Float")); - ar_integer_type = rb_const_get_at(ar_type, rb_intern("Integer")); - ar_boolean_type = rb_const_get_at(ar_type, rb_intern("Boolean")); - ar_date_time_type = rb_const_get_at(ar_type, rb_intern("DateTime")); - - ar_type_methods = rb_class_instance_methods(0, NULL, ar_string_type); - if (rb_ary_includes(ar_type_methods, - rb_to_symbol(rb_str_new_cstr("deserialize")))) { - deserialize_from_db_id = rb_intern("deserialize"); - } else { - deserialize_from_db_id = rb_intern("type_cast_from_database"); - } - - if (rb_const_defined_at(ar_type, rb_intern("Json")) == (int)Qtrue) { - ar_json_type = rb_const_get_at(ar_type, rb_intern("Json")); - } - - // TODO: if we get error or not, add this to some debug log - int isErrored; - rb_protect(cache_postgres_type_lookup, ar, &isErrored); - - rb_protect(cache_time_zone_type_lookup, ar, &isErrored); -} - -bool is_string_or_text_type(VALUE type_klass) { - return type_klass == ar_string_type || type_klass == ar_text_type || - (ar_pg_uuid_type != Qundef && type_klass == ar_pg_uuid_type); -} - -VALUE cast_string_or_text_type(VALUE value) { - if (RB_TYPE_P(value, T_STRING)) { - return value; - } - - if (value == Qtrue) { - return rb_str_new_cstr("t"); - } - - if (value == Qfalse) { - return rb_str_new_cstr("f"); - } - - return rb_funcall(value, to_s_id, 0); -} - -bool is_float_type(VALUE type_klass) { - return type_klass == ar_float_type || - (ar_pg_float_type != Qundef && type_klass == ar_pg_float_type); -} - -VALUE cast_float_type(VALUE value) { - if (RB_TYPE_P(value, T_FLOAT)) { - return value; - } - - if (RB_TYPE_P(value, T_STRING)) { - const char* val = StringValuePtr(value); - return rb_float_new(strtod(val, NULL)); - } - - return Qundef; -} - -bool is_integer_type(VALUE type_klass) { - return type_klass == ar_integer_type || - (ar_pg_integer_type != Qundef && type_klass == ar_pg_integer_type); -} - -VALUE cast_integer_type(VALUE value) { - if (RB_INTEGER_TYPE_P(value)) { - return value; - } - - if (RB_TYPE_P(value, T_STRING)) { - const char* val = StringValuePtr(value); - if (strlen(val) == 0) { - return Qnil; - } - return rb_cstr2inum(val, 10); - } - - if (RB_FLOAT_TYPE_P(value)) { - // We are calling the `to_i` here, because ruby internal - // `flo_to_i` is not accessible - return rb_funcall(value, to_i_id, 0); - } - - if (value == Qtrue) { - return INT2NUM(1); - } - - if (value == Qfalse) { - return INT2NUM(0); - } - - // At this point, we handled integer, float, string and booleans - // any thing other than this (array, hashes, etc) should result in nil - return Qnil; -} - -bool is_json_type(VALUE type_klass) { - return ((ar_pg_json_type != Qundef && type_klass == ar_pg_json_type) || - (ar_pg_jsonb_type != Qundef && type_klass == ar_pg_jsonb_type) || - (ar_json_type != Qundef && type_klass == ar_json_type)); -} - -bool is_boolean_type(VALUE type_klass) { return type_klass == ar_boolean_type; } - -VALUE cast_boolean_type(VALUE value) { - if (value == Qtrue || value == Qfalse) { - return value; - } - - if (value == Qnil) { - return Qnil; - } - - if (RB_TYPE_P(value, T_STRING)) { - if (RSTRING_LEN(value) == 0) { - return Qnil; - } - - const char* val = StringValuePtr(value); - - bool isFalseValue = - (*val == '0' || (*val == 'f' || *val == 'F') || - (strcmp(val, "false") == 0 || strcmp(val, "FALSE") == 0) || - (strcmp(val, "off") == 0 || strcmp(val, "OFF") == 0)); - - return isFalseValue ? Qfalse : Qtrue; - } - - if (RB_INTEGER_TYPE_P(value)) { - return value == INT2NUM(1) ? Qtrue : Qfalse; - } - - return Qundef; -} - -bool is_date_time_type(VALUE type_klass) { - return (type_klass == ar_date_time_type) || - (ar_pg_date_time_type != Qundef && - type_klass == ar_pg_date_time_type) || - (ar_pg_timestamp_type != Qundef && - type_klass == ar_pg_timestamp_type) || - (ar_time_zone_converter != Qundef && - type_klass == ar_time_zone_converter); -} - -VALUE cast_date_time_type(VALUE value) { - // Instead of take strings to comparing them to time zones - // and then comparing them back to string - // We will just make sure we have string on ISO8601 and it's utc - if (RB_TYPE_P(value, T_STRING)) { - const char* val = StringValuePtr(value); - // 'Z' in ISO8601 says it's UTC - if (val[strlen(val) - 1] == 'Z' && is_iso8601_time_string(val) == Qtrue) { - return value; - } - - volatile VALUE iso8601_string = iso_ar_iso_datetime_string(val); - if (iso8601_string != Qnil) { - return iso8601_string; - } - } - - return Qundef; -} - -VALUE rescue_func() { return Qfalse; } - -VALUE parse_json(VALUE value) { - return rb_funcall(oj_type, oj_sc_parse_id, 2, rb_cObject, value); -} - -VALUE is_json_value(VALUE value) { - if (!RB_TYPE_P(value, T_STRING)) { - return value; - } - - if (RSTRING_LEN(value) == 0) { - return Qfalse; - } - - volatile VALUE result = - rb_rescue2(parse_json, value, rescue_func, Qundef, oj_parseerror_type, 0); - - if (NIL_P(result)) { - return Qtrue; - } - - if (result == Qfalse) { - return Qfalse; - } - - // TODO: fix me! - return Qfalse; -} - -VALUE type_cast(VALUE type_metadata, VALUE value, volatile VALUE* isJson) { - if (value == Qnil || value == Qundef) { - return value; - } - - cache_type_lookup(); - - VALUE type_klass, typeCastedValue; - - type_klass = CLASS_OF(type_metadata); - typeCastedValue = Qundef; - - TypeCast typeCast; - for (typeCast = type_casts; typeCast->canCast != NULL; typeCast++) { - if (typeCast->canCast(type_klass)) { - typeCastedValue = typeCast->typeCast(value); - break; - } - } - - if (is_json_type(type_klass)) { - if (is_json_value(value) == Qfalse) { - return Qnil; - } - *isJson = Qtrue; - return value; - } - - if (typeCastedValue == Qundef) { - return rb_funcall(type_metadata, deserialize_from_db_id, 1, value); - } - - return typeCastedValue; -} - -VALUE public_type_cast(int argc, VALUE* argv, VALUE self) { - VALUE type_metadata, value, isJson; - rb_scan_args(argc, argv, "21", &type_metadata, &value, &isJson); - - if (isJson == Qnil || isJson == Qundef) { - isJson = Qfalse; - } - - return type_cast(type_metadata, value, &isJson); -} - -void panko_init_type_cast(VALUE mPanko) { - to_s_id = rb_intern("to_s"); - to_i_id = rb_intern("to_i"); - - oj_type = rb_const_get_at(rb_cObject, rb_intern("Oj")); - oj_parseerror_type = rb_const_get_at(oj_type, rb_intern("ParseError")); - oj_sc_parse_id = rb_intern("sc_parse"); - - // TODO: pass 3 arguments here - rb_define_singleton_method(mPanko, "_type_cast", public_type_cast, -1); - - panko_init_time(mPanko); - - rb_global_variable(&oj_type); - rb_global_variable(&oj_parseerror_type); - rb_global_variable(&ar_string_type); - rb_global_variable(&ar_text_type); - rb_global_variable(&ar_float_type); - rb_global_variable(&ar_integer_type); - rb_global_variable(&ar_boolean_type); - rb_global_variable(&ar_date_time_type); - rb_global_variable(&ar_time_zone_converter); - rb_global_variable(&ar_json_type); - rb_global_variable(&ar_pg_integer_type); - rb_global_variable(&ar_pg_float_type); - rb_global_variable(&ar_pg_uuid_type); - rb_global_variable(&ar_pg_json_type); - rb_global_variable(&ar_pg_jsonb_type); - rb_global_variable(&ar_pg_array_type); - rb_global_variable(&ar_pg_date_time_type); - rb_global_variable(&ar_pg_timestamp_type); -} diff --git a/ext/panko_serializer/attributes_writer/type_cast/type_cast.h b/ext/panko_serializer/attributes_writer/type_cast/type_cast.h deleted file mode 100644 index 0c3df228..00000000 --- a/ext/panko_serializer/attributes_writer/type_cast/type_cast.h +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include -#include - -/* - * Type Casting - * - * We do "special" type casting which is mix of two inspirations: - * *) light records gem - * *) pg TextDecoders - * - * The whole idea behind those type casts, are to do the minimum required - * type casting in the most performant manner and *allocation free*. - * - * For example, in `ActiveRecord::Type::String` the type_cast_from_database - * creates new string, for known reasons, but, in serialization flow we don't - * need to create new string becuase we afraid of mutations. - * - * Since we know before hand, that we are only reading from the database, and - * *not* writing and the end result if for JSON we can skip some "defenses". - */ - -typedef bool (*TypeMatchFunc)(VALUE type_klass); - -/* - * TypeCastFunc - * - * @return VALUE casted value or Qundef if not casted - */ -typedef VALUE (*TypeCastFunc)(VALUE value); - -typedef struct _TypeCast { - TypeMatchFunc canCast; - TypeCastFunc typeCast; -}* TypeCast; - -// ActiveRecord::Type::String -// ActiveRecord::Type::Text -bool is_string_or_text_type(VALUE type_klass); -VALUE cast_string_or_text_type(VALUE value); - -// ActiveRecord::Type::Float -bool is_float_type(VALUE type_klass); -VALUE cast_float_type(VALUE value); - -// ActiveRecord::Type::Integer -bool is_integer_type(VALUE type_klass); -VALUE cast_integer_type(VALUE value); - -// ActiveRecord::ConnectoinAdapters::PostgreSQL::Json -bool is_json_type(VALUE type_klass); -VALUE cast_json_type(VALUE value); - -// ActiveRecord::Type::Boolean -bool is_boolean_type(VALUE type_klass); -VALUE cast_boolean_type(VALUE value); - -// ActiveRecord::Type::DateTime -// ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime -// ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter -bool is_date_time_type(VALUE type_klass); -VALUE cast_date_time_type(VALUE value); - -static struct _TypeCast type_casts[] = { - {is_string_or_text_type, cast_string_or_text_type}, - {is_integer_type, cast_integer_type}, - {is_boolean_type, cast_boolean_type}, - {is_date_time_type, cast_date_time_type}, - {is_float_type, cast_float_type}, - - {NULL, NULL}}; - -extern VALUE type_cast(VALUE type_metadata, VALUE value, - volatile VALUE* isJson); -void panko_init_type_cast(VALUE mPanko); - -// Introduced in ruby 2.4 -#ifndef RB_INTEGER_TYPE_P -#define RB_INTEGER_TYPE_P(obj) (RB_FIXNUM_P(obj) || RB_TYPE_P(obj, T_BIGNUM)) -#endif diff --git a/ext/panko_serializer/common.h b/ext/panko_serializer/common.h deleted file mode 100644 index e3899693..00000000 --- a/ext/panko_serializer/common.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -#define PANKO_SAFE_HASH_SIZE(hash) \ - (hash == Qnil || hash == Qundef) ? 0 : RHASH_SIZE(hash) - -#define PANKO_EMPTY_HASH(hash) \ - (hash == Qnil || hash == Qundef) ? 1 : (RHASH_SIZE(hash) == 0) diff --git a/ext/panko_serializer/panko_serializer.c b/ext/panko_serializer/panko_serializer.c index c0220e97..febb20ab 100644 --- a/ext/panko_serializer/panko_serializer.c +++ b/ext/panko_serializer/panko_serializer.c @@ -1,181 +1,22 @@ -#include "panko_serializer.h" - #include -static ID push_value_id; -static ID push_array_id; -static ID push_object_id; -static ID push_json_id; -static ID pop_id; - -static ID to_a_id; - -static ID object_id; -static ID serialization_context_id; - -static VALUE SKIP = Qundef; - -void write_value(VALUE str_writer, VALUE key, VALUE value, VALUE isJson) { - if (isJson == Qtrue) { - rb_funcall(str_writer, push_json_id, 2, value, key); - } else { - rb_funcall(str_writer, push_value_id, 2, value, key); - } -} - -void serialize_method_fields(VALUE object, VALUE str_writer, - SerializationDescriptor descriptor) { - if (RARRAY_LEN(descriptor->method_fields) == 0) { - return; - } - - volatile VALUE method_fields, serializer, key; - long i; - - method_fields = descriptor->method_fields; - - serializer = descriptor->serializer; - rb_ivar_set(serializer, object_id, object); - - for (i = 0; i < RARRAY_LEN(method_fields); i++) { - volatile VALUE raw_attribute = RARRAY_AREF(method_fields, i); - Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute); - - volatile VALUE result = rb_funcall(serializer, attribute->name_id, 0); - if (result != SKIP) { - key = attr_name_for_serialization(attribute); - write_value(str_writer, key, result, Qfalse); - } - } - - rb_ivar_set(serializer, object_id, Qnil); -} - -void serialize_fields(VALUE object, VALUE str_writer, - SerializationDescriptor descriptor) { - descriptor->attributes_writer.write_attributes(object, descriptor->attributes, - write_value, str_writer); - - serialize_method_fields(object, str_writer, descriptor); -} - -void serialize_has_one_associations(VALUE object, VALUE str_writer, - VALUE associations) { - long i; - for (i = 0; i < RARRAY_LEN(associations); i++) { - volatile VALUE association_el = RARRAY_AREF(associations, i); - Association association = association_read(association_el); - - volatile VALUE value = rb_funcall(object, association->name_id, 0); - - if (NIL_P(value)) { - write_value(str_writer, association->name_str, value, Qfalse); - } else { - serialize_object(association->name_str, value, str_writer, - association->descriptor); - } - } -} - -void serialize_has_many_associations(VALUE object, VALUE str_writer, - VALUE associations) { - long i; - for (i = 0; i < RARRAY_LEN(associations); i++) { - volatile VALUE association_el = RARRAY_AREF(associations, i); - Association association = association_read(association_el); +#include "time_conversion.h" - volatile VALUE value = rb_funcall(object, association->name_id, 0); - - if (NIL_P(value)) { - write_value(str_writer, association->name_str, value, Qfalse); - } else { - serialize_objects(association->name_str, value, str_writer, - association->descriptor); - } - } -} - -VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer, - SerializationDescriptor descriptor) { - sd_set_writer(descriptor, object); - - rb_funcall(str_writer, push_object_id, 1, key); - - serialize_fields(object, str_writer, descriptor); - - if (RARRAY_LEN(descriptor->has_one_associations) > 0) { - serialize_has_one_associations(object, str_writer, - descriptor->has_one_associations); - } - - if (RARRAY_LEN(descriptor->has_many_associations) > 0) { - serialize_has_many_associations(object, str_writer, - descriptor->has_many_associations); - } - - rb_funcall(str_writer, pop_id, 0); - - return Qnil; -} - -VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer, - SerializationDescriptor descriptor) { - long i; - - rb_funcall(str_writer, push_array_id, 1, key); - - if (!RB_TYPE_P(objects, T_ARRAY)) { - objects = rb_funcall(objects, to_a_id, 0); - } - - for (i = 0; i < RARRAY_LEN(objects); i++) { - volatile VALUE object = RARRAY_AREF(objects, i); - serialize_object(Qnil, object, str_writer, descriptor); - } - - rb_funcall(str_writer, pop_id, 0); - - return Qnil; +VALUE public_is_iso8601_time_string(VALUE klass, VALUE value) { + return is_iso8601_time_string(StringValuePtr(value)) ? Qtrue : Qfalse; } -VALUE serialize_object_api(VALUE klass, VALUE object, VALUE str_writer, - VALUE descriptor) { - SerializationDescriptor sd = sd_read(descriptor); - return serialize_object(Qnil, object, str_writer, sd); -} - -VALUE serialize_objects_api(VALUE klass, VALUE objects, VALUE str_writer, - VALUE descriptor) { - serialize_objects(Qnil, objects, str_writer, sd_read(descriptor)); - - return Qnil; +VALUE public_iso_ar_iso_datetime_string(VALUE klass, VALUE value) { + return iso_ar_iso_datetime_string(StringValuePtr(value)); } void Init_panko_serializer() { - push_value_id = rb_intern("push_value"); - push_array_id = rb_intern("push_array"); - push_object_id = rb_intern("push_object"); - push_json_id = rb_intern("push_json"); - pop_id = rb_intern("pop"); - to_a_id = rb_intern("to_a"); - object_id = rb_intern("@object"); - serialization_context_id = rb_intern("@serialization_context"); - VALUE mPanko = rb_define_module("Panko"); - rb_define_singleton_method(mPanko, "serialize_object", serialize_object_api, - 3); - - rb_define_singleton_method(mPanko, "serialize_objects", serialize_objects_api, - 3); - - VALUE mPankoSerializer = rb_const_get(mPanko, rb_intern("Serializer")); - SKIP = rb_const_get(mPankoSerializer, rb_intern("SKIP")); - rb_global_variable(&SKIP); + rb_define_singleton_method(mPanko, "is_iso8601_time_string", + public_is_iso8601_time_string, 1); + rb_define_singleton_method(mPanko, "iso_ar_iso_datetime_string", + public_iso_ar_iso_datetime_string, 1); - panko_init_serialization_descriptor(mPanko); - init_attributes_writer(mPanko); - panko_init_type_cast(mPanko); - panko_init_attribute(mPanko); - panko_init_association(mPanko); + panko_init_time(mPanko); } diff --git a/ext/panko_serializer/panko_serializer.h b/ext/panko_serializer/panko_serializer.h deleted file mode 100644 index 866920cd..00000000 --- a/ext/panko_serializer/panko_serializer.h +++ /dev/null @@ -1,12 +0,0 @@ -#include - -#include "attributes_writer/attributes_writer.h" -#include "serialization_descriptor/association.h" -#include "serialization_descriptor/attribute.h" -#include "serialization_descriptor/serialization_descriptor.h" - -VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer, - SerializationDescriptor descriptor); - -VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer, - SerializationDescriptor descriptor); diff --git a/ext/panko_serializer/serialization_descriptor/association.c b/ext/panko_serializer/serialization_descriptor/association.c deleted file mode 100644 index bab5e356..00000000 --- a/ext/panko_serializer/serialization_descriptor/association.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "association.h" - -VALUE cAssociation; - -static void association_free(void* ptr) { - if (!ptr) { - return; - } - - Association association = (Association)ptr; - association->name_str = Qnil; - association->name_id = 0; - association->name_sym = Qnil; - association->rb_descriptor = Qnil; - - if (!association->descriptor || association->descriptor != NULL) { - association->descriptor = NULL; - } - - xfree(association); -} - -void association_mark(Association data) { - rb_gc_mark(data->name_str); - rb_gc_mark(data->name_sym); - rb_gc_mark(data->rb_descriptor); - - if (data->descriptor != NULL) { - sd_mark(data->descriptor); - } -} - -static VALUE association_new(int argc, VALUE* argv, VALUE self) { - Association association; - - Check_Type(argv[0], T_SYMBOL); - Check_Type(argv[1], T_STRING); - - association = ALLOC(struct _Association); - association->name_sym = argv[0]; - association->name_str = argv[1]; - association->rb_descriptor = argv[2]; - - association->name_id = rb_intern_str(rb_sym2str(association->name_sym)); - association->descriptor = sd_read(association->rb_descriptor); - - return Data_Wrap_Struct(cAssociation, association_mark, association_free, - association); -} - -Association association_read(VALUE association) { - return (Association)DATA_PTR(association); -} - -VALUE association_name_sym_ref(VALUE self) { - Association association = (Association)DATA_PTR(self); - return association->name_sym; -} - -VALUE association_name_str_ref(VALUE self) { - Association association = (Association)DATA_PTR(self); - return association->name_str; -} - -VALUE association_descriptor_ref(VALUE self) { - Association association = (Association)DATA_PTR(self); - return association->rb_descriptor; -} - -VALUE association_decriptor_aset(VALUE self, VALUE descriptor) { - Association association = (Association)DATA_PTR(self); - - association->rb_descriptor = descriptor; - association->descriptor = sd_read(descriptor); - - return association->rb_descriptor; -} - -void panko_init_association(VALUE mPanko) { - cAssociation = rb_define_class_under(mPanko, "Association", rb_cObject); - rb_undef_alloc_func(cAssociation); - rb_global_variable(&cAssociation); - - rb_define_module_function(cAssociation, "new", association_new, -1); - - rb_define_method(cAssociation, "name_sym", association_name_sym_ref, 0); - rb_define_method(cAssociation, "name_str", association_name_str_ref, 0); - rb_define_method(cAssociation, "descriptor", association_descriptor_ref, 0); - rb_define_method(cAssociation, "descriptor=", association_decriptor_aset, 1); -} diff --git a/ext/panko_serializer/serialization_descriptor/association.h b/ext/panko_serializer/serialization_descriptor/association.h deleted file mode 100644 index deaa202f..00000000 --- a/ext/panko_serializer/serialization_descriptor/association.h +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#ifndef __ASSOCIATION_H__ -#define __ASSOCIATION_H__ - -#include "serialization_descriptor.h" - -typedef struct _Association { - ID name_id; - VALUE name_sym; - VALUE name_str; - - VALUE rb_descriptor; - SerializationDescriptor descriptor; -}* Association; - -Association association_read(VALUE association); -void panko_init_association(VALUE mPanko); - -#endif diff --git a/ext/panko_serializer/serialization_descriptor/attribute.c b/ext/panko_serializer/serialization_descriptor/attribute.c deleted file mode 100644 index d22ecf7e..00000000 --- a/ext/panko_serializer/serialization_descriptor/attribute.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "attribute.h" - -ID attribute_aliases_id = 0; -VALUE cAttribute; - -static void attribute_free(void* ptr) { - if (!ptr) { - return; - } - - Attribute attribute = (Attribute)ptr; - attribute->name_str = Qnil; - attribute->name_id = 0; - attribute->alias_name = Qnil; - attribute->type = Qnil; - attribute->record_class = Qnil; - - xfree(attribute); -} - -void attribute_mark(Attribute data) { - rb_gc_mark(data->name_str); - rb_gc_mark(data->alias_name); - rb_gc_mark(data->type); - rb_gc_mark(data->record_class); -} - -static VALUE attribute_new(int argc, VALUE* argv, VALUE self) { - Attribute attribute; - - Check_Type(argv[0], T_STRING); - if (argv[1] != Qnil) { - Check_Type(argv[1], T_STRING); - } - - attribute = ALLOC(struct _Attribute); - attribute->name_str = argv[0]; - attribute->name_id = rb_intern_str(attribute->name_str); - attribute->alias_name = argv[1]; - attribute->type = Qnil; - attribute->record_class = Qnil; - - return Data_Wrap_Struct(cAttribute, attribute_mark, attribute_free, - attribute); -} - -Attribute attribute_read(VALUE attribute) { - return (Attribute)DATA_PTR(attribute); -} - -void attribute_try_invalidate(Attribute attribute, VALUE new_record_class) { - if (attribute->record_class != new_record_class) { - attribute->type = Qnil; - attribute->record_class = new_record_class; - - // Once the record class is changed for this attribute, check if - // we attribute_aliases (from ActivRecord), if so fill in - // performance wise - this code should be called once (unless the serialzier - // is polymorphic) - volatile VALUE ar_aliases_hash = - rb_funcall(new_record_class, attribute_aliases_id, 0); - - if (!PANKO_EMPTY_HASH(ar_aliases_hash)) { - volatile VALUE aliasedValue = - rb_hash_aref(ar_aliases_hash, attribute->name_str); - if (aliasedValue != Qnil) { - attribute->alias_name = attribute->name_str; - attribute->name_str = aliasedValue; - attribute->name_id = rb_intern_str(attribute->name_str); - } - } - } -} - -VALUE attribute_name_ref(VALUE self) { - Attribute attribute = (Attribute)DATA_PTR(self); - return attribute->name_str; -} - -VALUE attribute_alias_name_ref(VALUE self) { - Attribute attribute = (Attribute)DATA_PTR(self); - return attribute->alias_name; -} - -void panko_init_attribute(VALUE mPanko) { - attribute_aliases_id = rb_intern("attribute_aliases"); - - cAttribute = rb_define_class_under(mPanko, "Attribute", rb_cObject); - rb_undef_alloc_func(cAttribute); - rb_global_variable(&cAttribute); - - rb_define_module_function(cAttribute, "new", attribute_new, -1); - - rb_define_method(cAttribute, "name", attribute_name_ref, 0); - rb_define_method(cAttribute, "alias_name", attribute_alias_name_ref, 0); -} diff --git a/ext/panko_serializer/serialization_descriptor/attribute.h b/ext/panko_serializer/serialization_descriptor/attribute.h deleted file mode 100644 index d3cf2e91..00000000 --- a/ext/panko_serializer/serialization_descriptor/attribute.h +++ /dev/null @@ -1,27 +0,0 @@ -#include - -#ifndef __ATTRIBUTE_H__ -#define __ATTRIBUTE_H__ - -#include "../common.h" - -typedef struct _Attribute { - VALUE name_str; - ID name_id; - VALUE alias_name; - - /* - * We will cache the activerecord type - * by the record_class - */ - VALUE type; - VALUE record_class; -}* Attribute; - -Attribute attribute_read(VALUE attribute); -void attribute_try_invalidate(Attribute attribute, VALUE new_record_class); -void panko_init_attribute(VALUE mPanko); - -#define PANKO_ATTRIBUTE_READ(attribute) (Attribute) DATA_PTR(attribute) - -#endif diff --git a/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c b/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c deleted file mode 100644 index 583d623d..00000000 --- a/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c +++ /dev/null @@ -1,177 +0,0 @@ -#include "serialization_descriptor.h" - -static ID object_id; -static ID sc_id; - -static void sd_free(SerializationDescriptor sd) { - if (!sd) { - return; - } - - sd->serializer = Qnil; - sd->serializer_type = Qnil; - sd->attributes = Qnil; - sd->method_fields = Qnil; - sd->has_one_associations = Qnil; - sd->has_many_associations = Qnil; - sd->aliases = Qnil; - xfree(sd); -} - -void sd_mark(SerializationDescriptor data) { - rb_gc_mark(data->serializer); - rb_gc_mark(data->serializer_type); - rb_gc_mark(data->attributes); - rb_gc_mark(data->method_fields); - rb_gc_mark(data->has_one_associations); - rb_gc_mark(data->has_many_associations); - rb_gc_mark(data->aliases); -} - -static VALUE sd_alloc(VALUE klass) { - SerializationDescriptor sd = ALLOC(struct _SerializationDescriptor); - - sd->serializer = Qnil; - sd->serializer_type = Qnil; - sd->attributes = Qnil; - sd->method_fields = Qnil; - sd->has_one_associations = Qnil; - sd->has_many_associations = Qnil; - sd->aliases = Qnil; - - sd->attributes_writer = create_empty_attributes_writer(); - - return Data_Wrap_Struct(klass, sd_mark, sd_free, sd); -} - -SerializationDescriptor sd_read(VALUE descriptor) { - return (SerializationDescriptor)DATA_PTR(descriptor); -} - -void sd_set_writer(SerializationDescriptor sd, VALUE object) { - if (sd->attributes_writer.object_type != UnknownObjectType) { - return; - } - - sd->attributes_writer = create_attributes_writer(object); -} - -VALUE sd_serializer_set(VALUE self, VALUE serializer) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - - sd->serializer = serializer; - return Qnil; -} - -VALUE sd_serializer_ref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - - return sd->serializer; -} - -VALUE sd_attributes_set(VALUE self, VALUE attributes) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - - sd->attributes = attributes; - return Qnil; -} - -VALUE sd_attributes_ref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->attributes; -} - -VALUE sd_method_fields_set(VALUE self, VALUE method_fields) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - sd->method_fields = method_fields; - return Qnil; -} - -VALUE sd_method_fields_ref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->method_fields; -} - -VALUE sd_has_one_associations_set(VALUE self, VALUE has_one_associations) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - sd->has_one_associations = has_one_associations; - return Qnil; -} - -VALUE sd_has_one_associations_ref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->has_one_associations; -} - -VALUE sd_has_many_associations_set(VALUE self, VALUE has_many_associations) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - sd->has_many_associations = has_many_associations; - return Qnil; -} - -VALUE sd_has_many_associations_ref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->has_many_associations; -} - -VALUE sd_type_set(VALUE self, VALUE type) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - sd->serializer_type = type; - return Qnil; -} - -VALUE sd_type_aref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->serializer_type; -} - -VALUE sd_aliases_set(VALUE self, VALUE aliases) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - sd->aliases = aliases; - return Qnil; -} - -VALUE sd_aliases_aref(VALUE self) { - SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self); - return sd->aliases; -} - -void panko_init_serialization_descriptor(VALUE mPanko) { - object_id = rb_intern("@object"); - sc_id = rb_intern("@sc"); - - VALUE cSerializationDescriptor = - rb_define_class_under(mPanko, "SerializationDescriptor", rb_cObject); - - rb_define_alloc_func(cSerializationDescriptor, sd_alloc); - rb_define_method(cSerializationDescriptor, "serializer=", sd_serializer_set, - 1); - rb_define_method(cSerializationDescriptor, "serializer", sd_serializer_ref, - 0); - - rb_define_method(cSerializationDescriptor, "attributes=", sd_attributes_set, - 1); - rb_define_method(cSerializationDescriptor, "attributes", sd_attributes_ref, - 0); - - rb_define_method(cSerializationDescriptor, - "method_fields=", sd_method_fields_set, 1); - rb_define_method(cSerializationDescriptor, "method_fields", - sd_method_fields_ref, 0); - - rb_define_method(cSerializationDescriptor, - "has_one_associations=", sd_has_one_associations_set, 1); - rb_define_method(cSerializationDescriptor, "has_one_associations", - sd_has_one_associations_ref, 0); - - rb_define_method(cSerializationDescriptor, - "has_many_associations=", sd_has_many_associations_set, 1); - rb_define_method(cSerializationDescriptor, "has_many_associations", - sd_has_many_associations_ref, 0); - - rb_define_method(cSerializationDescriptor, "type=", sd_type_set, 1); - rb_define_method(cSerializationDescriptor, "type", sd_type_aref, 0); - - rb_define_method(cSerializationDescriptor, "aliases=", sd_aliases_set, 1); - rb_define_method(cSerializationDescriptor, "aliases", sd_aliases_aref, 0); -} diff --git a/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h b/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h deleted file mode 100644 index 6383ec8f..00000000 --- a/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include "attributes_writer/attributes_writer.h" - -typedef struct _SerializationDescriptor { - // type of the serializer, so we can create it later - VALUE serializer_type; - // Cached value of the serializer - VALUE serializer; - - // Metadata - VALUE attributes; - VALUE aliases; - VALUE method_fields; - VALUE has_one_associations; - VALUE has_many_associations; - - AttributesWriter attributes_writer; -}* SerializationDescriptor; - -SerializationDescriptor sd_read(VALUE descriptor); - -void sd_mark(SerializationDescriptor data); - -void sd_set_writer(SerializationDescriptor sd, VALUE object); - -void panko_init_serialization_descriptor(VALUE mPanko); diff --git a/ext/panko_serializer/attributes_writer/type_cast/time_conversion.c b/ext/panko_serializer/time_conversion.c similarity index 100% rename from ext/panko_serializer/attributes_writer/type_cast/time_conversion.c rename to ext/panko_serializer/time_conversion.c diff --git a/ext/panko_serializer/attributes_writer/type_cast/time_conversion.h b/ext/panko_serializer/time_conversion.h similarity index 100% rename from ext/panko_serializer/attributes_writer/type_cast/time_conversion.h rename to ext/panko_serializer/time_conversion.h diff --git a/lib/panko/array_serializer.rb b/lib/panko/array_serializer.rb index e5d2ba56..52d92ddb 100644 --- a/lib/panko/array_serializer.rb +++ b/lib/panko/array_serializer.rb @@ -45,7 +45,8 @@ def serialize_to_json(subjects) private def serialize_with_writer(subjects, writer) - Panko.serialize_objects(subjects.to_a, writer, @descriptor) + srz = Panko::Impl::Serializer.new(@descriptor) + srz.serialize_many(objects: subjects.to_a, writer: writer) writer end end diff --git a/lib/panko/association.rb b/lib/panko/association.rb index 35243c76..863ea4ba 100644 --- a/lib/panko/association.rb +++ b/lib/panko/association.rb @@ -2,6 +2,19 @@ module Panko class Association + attr_reader :name_sym, :name_str, :descriptor + attr_writer :descriptor + + def initialize(name_sym, name_str, descriptor) + @name_sym = name_sym + @name_str = name_str + @descriptor = descriptor + end + + def serializer_writer + @serializer_writer ||= Impl::Serializer.new(@descriptor) + end + def duplicate Panko::Association.new( name_sym, diff --git a/lib/panko/attribute.rb b/lib/panko/attribute.rb index d83112c9..0c04126a 100644 --- a/lib/panko/attribute.rb +++ b/lib/panko/attribute.rb @@ -2,20 +2,59 @@ module Panko class Attribute + attr_reader :name, :name_sym, :alias_name + attr_accessor :type + + def initialize(name, alias_name = nil) + # TODO: validate name & alias_name are strings + + self.name = name + @alias_name = alias_name + + @type = nil + @record_class = nil + end + def self.create(name, alias_name: nil) alias_name = alias_name.to_s unless alias_name.nil? Attribute.new(name.to_s, alias_name) end def ==(other) - return name.to_sym == other if other.is_a? Symbol - return name == other.name && alias_name == other.alias_name if other.is_a? Panko::Attribute + return name_sym == other if other.is_a? Symbol + return @name == other.name && @alias_name == other.alias_name if other.is_a? Panko::Attribute super end + # TODO: this logic is specific to ActiveRecord attributes writer and shouldn't be here. + def invalidate!(new_object_class) + return if @record_class == new_object_class + + @type = nil + @record_class = new_object_class + + # Once the record class is changed for this attribute, check if + # we attribute_aliases (from ActivRecord), if so fill in + # performance wise - this code should be called once (unless the serialzier + # is polymorphic) + aliases_hash = @record_class.attribute_aliases + return if aliases_hash.empty? + + aliased_value = aliases_hash[@name] + if aliased_value.present? + @alias_name = @name + self.name = aliased_value + end + end + def hash - name.to_sym.hash + @name_sym.hash + end + + def name_for_serialization + return @alias_name unless @alias_name.nil? + @name end def eql?(other) @@ -23,7 +62,14 @@ def eql?(other) end def inspect - "" + "" + end + + private + + def name=(name) + @name = name + @name_sym = name.to_sym end end end diff --git a/lib/panko/impl/attributes_writer/active_record/context.rb b/lib/panko/impl/attributes_writer/active_record/context.rb new file mode 100644 index 00000000..f090d621 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/context.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord + # TODO: this is a bad name for the class. + class Context + attr_reader :attributes_hash, :types, :additional_types, :values + + def initialize(record) + attributes_set = record.instance_variable_get(:@attributes) + attributes_hash = attributes_set.instance_variable_get(:@attributes) + + @attributes_hash = (attributes_hash.nil? || attributes_hash.empty?) ? {} : attributes_hash + @attributes_hash_size = @attributes_hash.size + + @types = attributes_set.instance_variable_get(:@types) + @additional_types = attributes_set.instance_variable_get(:@additional_types) + @try_to_read_from_additional_types = !@additional_types.nil? && !@additional_types.empty? + + @values = attributes_set.instance_variable_get(:@values) + @is_indexed_row = false + @indexed_row_column_indexes = nil + @indexed_row_row = nil + + # Check if the values are of type ActiveRecord::Result::IndexedRow + if defined?(ActiveRecord::Result::IndexedRow) && @values.is_a?(ActiveRecord::Result::IndexedRow) + @indexed_row_column_indexes = @values.instance_variable_get(:@column_indexes) + @indexed_row_row = @values.instance_variable_get(:@row) + @is_indexed_row = true + end + end + + # Reads a value from the indexed row + def read_value_from_indexed_row(member) + return nil if @indexed_row_column_indexes.nil? || @indexed_row_row.nil? + + column_index = @indexed_row_column_indexes[member] + return nil if column_index.nil? + + row = @indexed_row_row + return nil if row.nil? + + row[column_index] + end + + # Reads the attribute value + def read_attribute(attribute) + member = attribute.name + value = nil + + # If we have a populated attributes_hash + if !@attributes_hash.nil? && @attributes_hash_size > 0 + attribute_metadata = @attributes_hash[member] + unless attribute_metadata.nil? + value = attribute_metadata.instance_variable_get(:@value_before_type_cast) + attribute.type ||= attribute_metadata.instance_variable_get(:@type) + end + end + + # Fallback to reading from values or indexed row + if value.nil? && !@values.nil? + value = if @is_indexed_row + read_value_from_indexed_row(member) + else + @values[member] + end + end + + # Fetch the type if not yet set + if attribute.type.nil? && !value.nil? + if @try_to_read_from_additional_types + attribute.type = @additional_types[member] + end + + attribute.type ||= @types[member] + end + + value + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/boolean_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/boolean_writer.rb new file mode 100644 index 00000000..74a218c0 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/boolean_writer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class BooleanWriter + def write(value, writer, key) + # TODO: compare against class maybe.. + if value == true || value == false + writer.push_value(value, key) + return true + end + + if value.nil? + writer.push_value(nil, key) + return true + end + + if value.is_a?(String) + return nil if value.length == 0 + + is_false_value = + value == "0" || (value == "f" || value == "F") || + (value.downcase == "false" || value.downcase == "off") + + writer.push_value(is_false_value ? false : true, key) + return true + end + + if value.is_a?(Integer) + writer.push_value(value == 1, key) + end + + false + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb new file mode 100644 index 00000000..d1596ab2 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class DateTimeWriter + def write(value, writer, key) + return false unless value.is_a?(String) + + if value.end_with?("Z") && Panko.is_iso8601_time_string(value) + writer.push_value(value, key) + return true + end + + iso8601_string = Panko.iso_ar_iso_datetime_string(value) + unless iso8601_string.nil? + writer.push_value(iso8601_string, key) + return true + end + + false + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/float_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/float_writer.rb new file mode 100644 index 00000000..d6fad3f3 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/float_writer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class FloatWriter + def write(value, writer, key) + if value.is_a?(Float) + writer.push_value(value, key) + true + elsif value.is_a?(String) + writer.push_value(value.to_f, key) + true + else + false + end + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/integer_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/integer_writer.rb new file mode 100644 index 00000000..8513f9b8 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/integer_writer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class IntegerWriter + def write(value, writer, key) + if value.is_a?(Integer) + writer.push_value(value, key) + return true + elsif value.is_a?(String) && !value.empty? + writer.push_value(value.to_i, key) + return true + elsif value.is_a?(Float) + writer.push_value(value.to_i, key) + return true + elsif value == true + writer.push_value(1, key) + return true + elsif value == false + writer.push_value(0, key) + return true + end + + # At this point, we handled integer, float, string and booleans + # any thing other than this (array, hashes, etc), so we should write nil. + writer.push_value(nil, key) + true + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/json_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/json_writer.rb new file mode 100644 index 00000000..2bfa1df2 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/json_writer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class JsonWriter + def write(value, writer, key) + if is_json_value?(value) + writer.push_json(value, key) + return true + end + + false + end + + private + + def is_json_value?(value) + return value unless value.is_a?(String) + + return false if value.length == 0 + + begin + result = Oj.sc_parse(Object.new, value) + + return true if result.nil? + return false if result == false + rescue Oj::ParseError + return false + end + + false + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/string_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/string_writer.rb new file mode 100644 index 00000000..06874f32 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/string_writer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + class StringWriter + def write(value, writer, key) + case value + when String + writer.push_value(value, key) + when TrueClass + writer.push_value("t", key) + when FalseClass + writer.push_value("f", key) + else + writer.push_value(value.to_s, key) + end + + # we always write.. we have "else" case + true + end + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb new file mode 100644 index 00000000..726a3708 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative "string_writer" +require_relative "integer_writer" +require_relative "float_writer" +require_relative "boolean_writer" +require_relative "datetime_writer" +require_relative "json_writer" + +module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter + # Type Casting + # + # We do "special" type casting which is mix of two inspirations: + # *) light records gem + # *) pg TextDecoders + # + # The whole idea behind those type casts, are to do the minimum required + # type casting in the most performant manner and *allocation free*. + # + # For example, in `ActiveRecord::Type::String` the type_cast_from_database + # creates new string, for known reasons, but, in serialization flow we don't + # need to create new string becuase we afraid of mutations. + # + # Since we know before hand, that we are only reading from the database, and + # *not* writing and the end result if for JSON we can skip some "defenses". + class Writer + def initialize + @string_writer = StringWriter.new + @integer_writer = IntegerWriter.new + @float_writer = FloatWriter.new + @boolean_writer = BooleanWriter.new + @date_time_writer = DateTimeWriter.new + @json_writer = JsonWriter.new + end + + def write(writer, attribute, value) + key = attribute.name_for_serialization + + if value.nil? + writer.push_value(nil, key) + return + end + + if attribute.type.nil? + writer.push_value(value, key) + return + end + + # TODO: validate against arrays + written = case attribute.type.type + when :string, :text, :uuid + @string_writer.write(value, writer, key) + when :integer + @integer_writer.write(value, writer, key) + when :float + @float_writer.write(value, writer, key) + when :boolean + @boolean_writer.write(value, writer, key) + when :datetime + @date_time_writer.write(value, writer, key) + when :json, :jsonb + @json_writer.write(value, writer, key) + else + false + end + + unless written + writer.push_value(attribute.type.deserialize(value), key) + end + end + end + + # TODO: maybe use `include Singleton` here + @@writer = Writer.new + + def self.write(writer, attribute, value) + @@writer.write(writer, attribute, value) + end +end diff --git a/lib/panko/impl/attributes_writer/active_record/writer.rb b/lib/panko/impl/attributes_writer/active_record/writer.rb new file mode 100644 index 00000000..7aa555e9 --- /dev/null +++ b/lib/panko/impl/attributes_writer/active_record/writer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "context" +require_relative "values_writer/writer" + +module Panko::Impl::AttributesWriter::ActiveRecord + class Writer + def write_attributes(object, descriptor, writer) + attributes_ctx = Context.new(object) + object_class = object.class + + descriptor.attributes.each do |attribute| + attribute.invalidate!(object_class) + + value = attributes_ctx.read_attribute(attribute) + + ValuesWriter.write(writer, attribute, value) + end + end + end +end diff --git a/lib/panko/impl/attributes_writer/creator.rb b/lib/panko/impl/attributes_writer/creator.rb new file mode 100644 index 00000000..861fbf91 --- /dev/null +++ b/lib/panko/impl/attributes_writer/creator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "hash_writer" +require_relative "plain_writer" +require_relative "active_record/writer" + +module Panko::Impl + module AttributesWriter + def self.create(object) + if defined?(::ActiveRecord::Base) && object.is_a?(::ActiveRecord::Base) + return ActiveRecord::Writer.new + end + + if object.is_a?(Hash) + return HashWriter.new + end + + PlainWriter.new + end + end +end diff --git a/lib/panko/impl/attributes_writer/hash_writer.rb b/lib/panko/impl/attributes_writer/hash_writer.rb new file mode 100644 index 00000000..b9a4f810 --- /dev/null +++ b/lib/panko/impl/attributes_writer/hash_writer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter + class HashWriter + def write_attributes(object, descriptor, writer) + descriptor.attributes.each do |attr| + value = object[attr.name] + writer.push_value(value, attr.name_for_serialization) + end + end + end +end diff --git a/lib/panko/impl/attributes_writer/plain_writer.rb b/lib/panko/impl/attributes_writer/plain_writer.rb new file mode 100644 index 00000000..6bedfc5e --- /dev/null +++ b/lib/panko/impl/attributes_writer/plain_writer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Panko::Impl::AttributesWriter + class PlainWriter + def write_attributes(object, descriptor, writer) + descriptor.attributes.each do |attr| + value = object.public_send(attr.name_sym) + writer.push_value(value, attr.name_for_serialization) + end + end + end +end diff --git a/lib/panko/impl/serializer.rb b/lib/panko/impl/serializer.rb new file mode 100644 index 00000000..46ee6143 --- /dev/null +++ b/lib/panko/impl/serializer.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Panko::Impl + SKIP = Object.new.freeze + + class Serializer + def initialize(descriptor) + @descriptor = descriptor + end + + def serialize_many(objects:, writer:, key: nil) + writer.push_array(key) + + objects.each do |object| + serialize_one(object: object, writer: writer) + end + + writer.pop + end + + def serialize_one(object:, writer:, key: nil) + writer.push_object(key) + + write_fields object, writer + write_method_fields object, writer + + serialize_has_one_association(object, writer) + serialize_has_many_associations(object, writer) + + writer.pop + end + + private + + def write_fields(object, writer) + if @descriptor.attributes_writer.nil? + @descriptor.attributes_writer = Panko::Impl::AttributesWriter.create(object) + end + + if @descriptor.attributes_writer.nil? + Panko._sd_set_writer(@descriptor, object) + Panko._write_attributes(object, @descriptor, writer) + else + @descriptor.attributes_writer.write_attributes(object, @descriptor, writer) + end + end + + def serialize_has_one_association(object, writer) + assocs = @descriptor.has_one_associations + return if assocs.empty? + + length = assocs.length + i = 0 + while i < length + assoc = assocs[i] + value = object.public_send(assoc.name_sym) + + if value.nil? + write_value writer, assoc.name_str, nil + else + assoc.serializer_writer.serialize_one object: value, writer: writer, key: assoc.name_str + end + i += 1 + end + end + + def serialize_has_many_associations(object, writer) + assocs = @descriptor.has_many_associations + return if assocs.empty? + + length = assocs.length + i = 0 + while i < length + assoc = assocs[i] + value = object.public_send(assoc.name_sym) + + if value.nil? + write_value writer, assoc.name_str, nil + else + assoc.serializer_writer.serialize_many objects: value, writer: writer, key: assoc.name_str + end + + i += 1 + end + end + + def write_method_fields(object, writer) + fields = @descriptor.method_fields + + return if fields.empty? + + serializer = @descriptor.serializer + serializer.instance_variable_set(:@object, object) + + length = fields.length + i = 0 + while i < length + method_field = fields[i] + result = serializer.public_send(method_field.name_sym) + + unless result == SKIP + key = method_field.name_for_serialization + write_value(writer, key, result) + end + + i += 1 + end + end + + def write_value(writer, key, value) + writer.push_value(value, key) + end + end +end diff --git a/lib/panko/serialization_descriptor.rb b/lib/panko/serialization_descriptor.rb index 16de3b2f..9ee7dd36 100644 --- a/lib/panko/serialization_descriptor.rb +++ b/lib/panko/serialization_descriptor.rb @@ -2,6 +2,27 @@ module Panko class SerializationDescriptor + attr_accessor :attributes, + :method_fields, + :has_one_associations, + :has_many_associations, + :aliases, + :type, + :serializer, + :attributes_writer + + def initialize + @attributes = [] + @method_fields = [] + @has_one_associations = [] + @has_many_associations = [] + # TODO: check if we need aliases + @aliases = [] + @type = nil + @serializer = nil + @attributes_writer = nil + end + # # Creates new description and apply the options # on the new descriptor @@ -131,16 +152,18 @@ def apply_association_filters(associations, only_filters, except_filters) end end + EMPTY_OBJECT = {}.freeze + def resolve_filters(options, filter) - filters = options.fetch(filter, {}) - return filters, {} if filters.is_a? Array + filters = options.fetch(filter, EMPTY_OBJECT) + return filters, EMPTY_OBJECT if filters.is_a? Array # hash filters looks like this # { instance: [:a], foo: [:b] } # which mean, for the current instance use `[:a]` as filter # and for association named `foo` use `[:b]` - return [], {} if filters.empty? + return [], EMPTY_OBJECT if filters.empty? attributes_filters = filters.fetch(:instance, []) association_filters = filters.except(:instance) diff --git a/lib/panko/serializer.rb b/lib/panko/serializer.rb index 19849b79..ac053af4 100644 --- a/lib/panko/serializer.rb +++ b/lib/panko/serializer.rb @@ -32,7 +32,7 @@ def context module Panko class Serializer - SKIP = Object.new.freeze + SKIP = Panko::Impl::SKIP class << self def inherited(base) @@ -138,7 +138,8 @@ def serialize_to_json(object) def serialize_with_writer(object, writer) raise ArgumentError.new("Panko::Serializer instances are single-use") if @used - Panko.serialize_object(object, writer, @descriptor) + srz = Panko::Impl::Serializer.new(@descriptor) + srz.serialize_one(object: object, writer: writer) @used = true writer end diff --git a/lib/panko_serializer.rb b/lib/panko_serializer.rb index 034a0231..d4643dc8 100644 --- a/lib/panko_serializer.rb +++ b/lib/panko_serializer.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true require "panko/version" + +# TODO: temporary patch. +module Panko::Impl +end +require "panko/impl/attributes_writer/creator" +require "panko/impl/serializer" + require "panko/attribute" require "panko/association" require "panko/serializer" diff --git a/spec/panko/type_cast_spec.rb b/spec/panko/type_cast_spec.rb index a14ae8fe..81a18a4e 100644 --- a/spec/panko/type_cast_spec.rb +++ b/spec/panko/type_cast_spec.rb @@ -18,27 +18,27 @@ def check_if_exists(module_name) context "ActiveRecord::Type::String" do let(:type) { ActiveRecord::Type::String.new } - it { expect(Panko._type_cast(type, true)).to eq("t") } - it { expect(Panko._type_cast(type, nil)).to be_nil } - it { expect(Panko._type_cast(type, false)).to eq("f") } - it { expect(Panko._type_cast(type, 123)).to eq("123") } - it { expect(Panko._type_cast(type, "hello world")).to eq("hello world") } + it { expect(type_cast(type, true)).to eq("t") } + it { expect(type_cast(type, nil)).to be_nil } + it { expect(type_cast(type, false)).to eq("f") } + it { expect(type_cast(type, 123)).to eq("123") } + it { expect(type_cast(type, "hello world")).to eq("hello world") } end context "ActiveRecord::Type::Text" do let(:type) { ActiveRecord::Type::Text.new } - it { expect(Panko._type_cast(type, true)).to eq("t") } - it { expect(Panko._type_cast(type, false)).to eq("f") } - it { expect(Panko._type_cast(type, 123)).to eq("123") } - it { expect(Panko._type_cast(type, "hello world")).to eq("hello world") } + it { expect(type_cast(type, true)).to eq("t") } + it { expect(type_cast(type, false)).to eq("f") } + it { expect(type_cast(type, 123)).to eq("123") } + it { expect(type_cast(type, "hello world")).to eq("hello world") } end # We treat uuid as stirng, there is no need for type cast before serialization context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid" do let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid.new } - it { expect(Panko._type_cast(type, "e67d284b-87b8-445e-a20d-3c76ea353866")).to eq("e67d284b-87b8-445e-a20d-3c76ea353866") } + it { expect(type_cast(type, "e67d284b-87b8-445e-a20d-3c76ea353866")).to eq("e67d284b-87b8-445e-a20d-3c76ea353866") } end end @@ -46,87 +46,87 @@ def check_if_exists(module_name) context "ActiveRecord::Type::Integer" do let(:type) { ActiveRecord::Type::Integer.new } - it { expect(Panko._type_cast(type, "")).to be_nil } - it { expect(Panko._type_cast(type, nil)).to be_nil } + it { expect(type_cast(type, "")).to be_nil } + it { expect(type_cast(type, nil)).to be_nil } - it { expect(Panko._type_cast(type, 1)).to eq(1) } - it { expect(Panko._type_cast(type, "1")).to eq(1) } - it { expect(Panko._type_cast(type, 1.7)).to eq(1) } + it { expect(type_cast(type, 1)).to eq(1) } + it { expect(type_cast(type, "1")).to eq(1) } + it { expect(type_cast(type, 1.7)).to eq(1) } - it { expect(Panko._type_cast(type, true)).to eq(1) } - it { expect(Panko._type_cast(type, false)).to eq(0) } + it { expect(type_cast(type, true)).to eq(1) } + it { expect(type_cast(type, false)).to eq(0) } - it { expect(Panko._type_cast(type, [6])).to be_nil } - it { expect(Panko._type_cast(type, six: 6)).to be_nil } + it { expect(type_cast(type, [6])).to be_nil } + it { expect(type_cast(type, six: 6)).to be_nil } end context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer", if: check_if_exists("ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer") do let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer.new } - it { expect(Panko._type_cast(type, "")).to be_nil } - it { expect(Panko._type_cast(type, nil)).to be_nil } + it { expect(type_cast(type, "")).to be_nil } + it { expect(type_cast(type, nil)).to be_nil } - it { expect(Panko._type_cast(type, 1)).to eq(1) } - it { expect(Panko._type_cast(type, "1")).to eq(1) } - it { expect(Panko._type_cast(type, 1.7)).to eq(1) } + it { expect(type_cast(type, 1)).to eq(1) } + it { expect(type_cast(type, "1")).to eq(1) } + it { expect(type_cast(type, 1.7)).to eq(1) } - it { expect(Panko._type_cast(type, true)).to eq(1) } - it { expect(Panko._type_cast(type, false)).to eq(0) } + it { expect(type_cast(type, true)).to eq(1) } + it { expect(type_cast(type, false)).to eq(0) } - it { expect(Panko._type_cast(type, [6])).to be_nil } - it { expect(Panko._type_cast(type, six: 6)).to be_nil } + it { expect(type_cast(type, [6])).to be_nil } + it { expect(type_cast(type, six: 6)).to be_nil } end end context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json", if: check_if_exists("ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json") do let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new } - it { expect(Panko._type_cast(type, "")).to be_nil } - it { expect(Panko._type_cast(type, "shnitzel")).to be_nil } - it { expect(Panko._type_cast(type, nil)).to be_nil } + it { expect(type_cast(type, "")).to be_nil } + it { expect(type_cast(type, "shnitzel")).to be_nil } + it { expect(type_cast(type, nil)).to be_nil } - it { expect(Panko._type_cast(type, '{"a":1}')).to eq('{"a":1}') } - it { expect(Panko._type_cast(type, "[6,12]")).to eq("[6,12]") } + it { expect(type_cast(type, '{"a":1}')).to eq('{"a":1}') } + it { expect(type_cast(type, "[6,12]")).to eq("[6,12]") } - it { expect(Panko._type_cast(type, "a" => 1)).to eq("a" => 1) } - it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) } + it { expect(type_cast(type, "a" => 1)).to eq("a" => 1) } + it { expect(type_cast(type, [6, 12])).to eq([6, 12]) } end context "ActiveRecord::Type::Json", if: check_if_exists("ActiveRecord::Type::Json") do let(:type) { ActiveRecord::Type::Json.new } - it { expect(Panko._type_cast(type, "")).to be_nil } - it { expect(Panko._type_cast(type, "shnitzel")).to be_nil } - it { expect(Panko._type_cast(type, nil)).to be_nil } + it { expect(type_cast(type, "")).to be_nil } + it { expect(type_cast(type, "shnitzel")).to be_nil } + it { expect(type_cast(type, nil)).to be_nil } - it { expect(Panko._type_cast(type, '{"a":1}')).to eq('{"a":1}') } - it { expect(Panko._type_cast(type, "[6,12]")).to eq("[6,12]") } + it { expect(type_cast(type, '{"a":1}')).to eq('{"a":1}') } + it { expect(type_cast(type, "[6,12]")).to eq("[6,12]") } - it { expect(Panko._type_cast(type, "a" => 1)).to eq("a" => 1) } - it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) } + it { expect(type_cast(type, "a" => 1)).to eq("a" => 1) } + it { expect(type_cast(type, [6, 12])).to eq([6, 12]) } end context "ActiveRecord::Type::Boolean" do let(:type) { ActiveRecord::Type::Boolean.new } - it { expect(Panko._type_cast(type, "")).to be_nil } - it { expect(Panko._type_cast(type, nil)).to be_nil } - - it { expect(Panko._type_cast(type, true)).to be_truthy } - it { expect(Panko._type_cast(type, 1)).to be_truthy } - it { expect(Panko._type_cast(type, "1")).to be_truthy } - it { expect(Panko._type_cast(type, "t")).to be_truthy } - it { expect(Panko._type_cast(type, "T")).to be_truthy } - it { expect(Panko._type_cast(type, "true")).to be_truthy } - it { expect(Panko._type_cast(type, "TRUE")).to be_truthy } - - it { expect(Panko._type_cast(type, false)).to be_falsey } - it { expect(Panko._type_cast(type, 0)).to be_falsey } - it { expect(Panko._type_cast(type, "0")).to be_falsey } - it { expect(Panko._type_cast(type, "f")).to be_falsey } - it { expect(Panko._type_cast(type, "F")).to be_falsey } - it { expect(Panko._type_cast(type, "false")).to be_falsey } - it { expect(Panko._type_cast(type, "FALSE")).to be_falsey } + it { expect(type_cast(type, "")).to be_nil } + it { expect(type_cast(type, nil)).to be_nil } + + it { expect(type_cast(type, true)).to be_truthy } + it { expect(type_cast(type, 1)).to be_truthy } + it { expect(type_cast(type, "1")).to be_truthy } + it { expect(type_cast(type, "t")).to be_truthy } + it { expect(type_cast(type, "T")).to be_truthy } + it { expect(type_cast(type, "true")).to be_truthy } + it { expect(type_cast(type, "TRUE")).to be_truthy } + + it { expect(type_cast(type, false)).to be_falsey } + it { expect(type_cast(type, 0)).to be_falsey } + it { expect(type_cast(type, "0")).to be_falsey } + it { expect(type_cast(type, "f")).to be_falsey } + it { expect(type_cast(type, "F")).to be_falsey } + it { expect(type_cast(type, "false")).to be_falsey } + it { expect(type_cast(type, "FALSE")).to be_falsey } end context "Time" do @@ -135,11 +135,11 @@ def check_if_exists(module_name) let(:utc) { ActiveSupport::TimeZone.new("UTC") } it "ISO8601 strings" do - expect(Panko._type_cast(type, date.in_time_zone(utc).as_json)).to eq("2017-03-04T12:45:23.000Z") + expect(type_cast(type, date.in_time_zone(utc).as_json)).to eq("2017-03-04T12:45:23.000Z") end it "two digits after ." do - expect(Panko._type_cast(type, "2018-09-16 14:51:03.97")).to eq("2018-09-16T14:51:03.970Z") + expect(type_cast(type, "2018-09-16 14:51:03.97")).to eq("2018-09-16T14:51:03.970Z") end it "converts string from datbase to utc time zone" do @@ -147,7 +147,44 @@ def check_if_exists(module_name) seconds = 40 + Rational(937_296, 10**6) result = DateTime.new(2017, 7, 10, 9, 26, seconds).in_time_zone(utc) - expect(Panko._type_cast(type, time)).to eq(result.as_json) + expect(type_cast(type, time)).to eq(result.as_json) end end end + +class TestWriter + attr_reader :value, :is_json + def initialize + @value = nil + @is_json = false + end + + def push_value(value, key) + @value = value + end + + def push_json(value, key) + @value = value + @is_json = true + end +end + +class TestAttribute + attr_reader :type + def initialize(type) + @type = type + end + + def name_for_serialization + "fake" + end +end + +def type_cast(type, value) + writer = TestWriter.new + attribute = TestAttribute.new(type) + + Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter.write(writer, attribute, value) + + writer.value +end From 79d91ddc1859a93db59d9dc1fe77625114d22369 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Sat, 26 Oct 2024 13:32:45 +0000 Subject: [PATCH 2/7] DateTimeWriter: write in native --- ext/panko_serializer/panko_serializer.c | 69 ++++++++++++++++--- .../values_writer/datetime_writer.rb | 15 +--- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/ext/panko_serializer/panko_serializer.c b/ext/panko_serializer/panko_serializer.c index febb20ab..6119594a 100644 --- a/ext/panko_serializer/panko_serializer.c +++ b/ext/panko_serializer/panko_serializer.c @@ -2,21 +2,72 @@ #include "time_conversion.h" -VALUE public_is_iso8601_time_string(VALUE klass, VALUE value) { - return is_iso8601_time_string(StringValuePtr(value)) ? Qtrue : Qfalse; +static ID push_value_id; + +static VALUE datetime_writer_write(VALUE self, VALUE value, VALUE writer, + VALUE key) { + if (RB_TYPE_P(value, T_STRING)) { + const char* val = StringValuePtr(value); + + // 'Z' in ISO8601 says it's UTC + if (val[strlen(val) - 1] == 'Z' && is_iso8601_time_string(val) == Qtrue) { + rb_funcall(writer, push_value_id, 2, value, key); + return Qtrue; + } + + volatile VALUE iso8601_string = iso_ar_iso_datetime_string(val); + if (iso8601_string != Qnil) { + rb_funcall(writer, push_value_id, 2, iso8601_string, key); + return Qtrue; + } + } + + return Qfalse; } -VALUE public_iso_ar_iso_datetime_string(VALUE klass, VALUE value) { - return iso_ar_iso_datetime_string(StringValuePtr(value)); +// Helper function to safely get a constant if it exists +static VALUE safe_const_get(VALUE parent, const char* name) { + if (rb_const_defined(parent, rb_intern(name))) { + return rb_const_get(parent, rb_intern(name)); + } + return Qnil; } void Init_panko_serializer() { - VALUE mPanko = rb_define_module("Panko"); + push_value_id = rb_intern("push_value"); - rb_define_singleton_method(mPanko, "is_iso8601_time_string", - public_is_iso8601_time_string, 1); - rb_define_singleton_method(mPanko, "iso_ar_iso_datetime_string", - public_iso_ar_iso_datetime_string, 1); + VALUE mPanko = rb_define_module("Panko"); panko_init_time(mPanko); + + VALUE impl = safe_const_get(mPanko, "Impl"); + if (NIL_P(impl)) { + printf("Not patching\n"); + return; + } + + VALUE attributes_writer = safe_const_get(impl, "AttributesWriter"); + if (NIL_P(attributes_writer)) { + printf("Not patching\n"); + return; + } + + VALUE active_record = safe_const_get(attributes_writer, "ActiveRecord"); + if (NIL_P(active_record)) { + printf("Not patching\n"); + return; + } + + VALUE values_writer = safe_const_get(active_record, "ValuesWriter"); + if (NIL_P(values_writer)) { + printf("Not patching\n"); + return; + } + + VALUE cDateTimeWriter = safe_const_get(values_writer, "DateTimeWriter"); + if (NIL_P(cDateTimeWriter)) { + printf("Not patching\n"); + } else { + rb_define_method(cDateTimeWriter, "write", datetime_writer_write, 3); + } } diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb index d1596ab2..f59be788 100644 --- a/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/datetime_writer.rb @@ -3,19 +3,8 @@ module Panko::Impl::AttributesWriter::ActiveRecord::ValuesWriter class DateTimeWriter def write(value, writer, key) - return false unless value.is_a?(String) - - if value.end_with?("Z") && Panko.is_iso8601_time_string(value) - writer.push_value(value, key) - return true - end - - iso8601_string = Panko.iso_ar_iso_datetime_string(value) - unless iso8601_string.nil? - writer.push_value(iso8601_string, key) - return true - end - + # The actual implementation is in the native extension + # if there is no native extension, we will fallback to Rails implementation false end end From d96205e99a8f88942f54a4c31d2d9b7af4bbf8c9 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Fri, 1 Nov 2024 21:21:12 +0000 Subject: [PATCH 3/7] attributes_writer/active_record: perf improvement * Reduce calls of `instance_variable_get` * Simplfiy iteration --- .../active_record/context.rb | 111 +++++++++++++++--- .../attributes_writer/active_record/writer.rb | 8 +- 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/lib/panko/impl/attributes_writer/active_record/context.rb b/lib/panko/impl/attributes_writer/active_record/context.rb index f090d621..118a63f6 100644 --- a/lib/panko/impl/attributes_writer/active_record/context.rb +++ b/lib/panko/impl/attributes_writer/active_record/context.rb @@ -1,31 +1,112 @@ # frozen_string_literal: true +begin + require "active_support" + require "active_support/rails" + require "active_model/attribute_set/builder" + + class ActiveModel::AttributeSet + def _panko_attributes_hash + @attributes + end + + def _panko_types + @types + end + + def _panko_additional_types + @additional_types + end + + def _panko_values + @values + end + end + + class ActiveModel::LazyAttributeSet + def _panko_attributes_hash + @attributes + end + + def _panko_types + @types + end + + def _panko_additional_types + @additional_types + end + + def _panko_values + @values + end + end +rescue => e + puts "FAILED to patch ActiveModel::LazyAttributeSet #{e}" + raise e +end + module Panko::Impl::AttributesWriter::ActiveRecord + begin + require "active_record" + + class ActiveRecord::Base + def _panko_attributes + @attributes + end + end + + if defined?(::ActiveRecord::Result::IndexedRow) + class ::ActiveRecord::Result::IndexedRow + def _panko_column_indexes + @column_indexes + end + + def _panko_row + @row + end + end + + PANKO_INDEX_ROW_DEFINED = true + else + PANKO_INDEX_ROW_DEFINED = false + end + rescue + PANKO_INDEX_ROW_DEFINED = false + end + + EMPTY_HASH = {}.freeze + # TODO: this is a bad name for the class. class Context attr_reader :attributes_hash, :types, :additional_types, :values def initialize(record) - attributes_set = record.instance_variable_get(:@attributes) - attributes_hash = attributes_set.instance_variable_get(:@attributes) - - @attributes_hash = (attributes_hash.nil? || attributes_hash.empty?) ? {} : attributes_hash - @attributes_hash_size = @attributes_hash.size + attributes_set = record._panko_attributes + + attributes_hash = attributes_set._panko_attributes_hash + if attributes_hash&.empty? + @attributes_hash = EMPTY_HASH + @attributes_hash_size = 0 + else + @attributes_hash = attributes_hash + @attributes_hash_size = attributes_hash.size + end - @types = attributes_set.instance_variable_get(:@types) - @additional_types = attributes_set.instance_variable_get(:@additional_types) + @types = attributes_set._panko_types + @additional_types = attributes_set._panko_additional_types @try_to_read_from_additional_types = !@additional_types.nil? && !@additional_types.empty? - @values = attributes_set.instance_variable_get(:@values) - @is_indexed_row = false - @indexed_row_column_indexes = nil - @indexed_row_row = nil + @values = attributes_set._panko_values # Check if the values are of type ActiveRecord::Result::IndexedRow - if defined?(ActiveRecord::Result::IndexedRow) && @values.is_a?(ActiveRecord::Result::IndexedRow) - @indexed_row_column_indexes = @values.instance_variable_get(:@column_indexes) - @indexed_row_row = @values.instance_variable_get(:@row) + if PANKO_INDEX_ROW_DEFINED && @values.is_a?(ActiveRecord::Result::IndexedRow) + @indexed_row_column_indexes = @values._panko_column_indexes + @indexed_row_row = @values._panko_row @is_indexed_row = true + else + @indexed_row_column_indexes = nil + @is_indexed_row = false + @indexed_row_row = nil end end @@ -48,7 +129,7 @@ def read_attribute(attribute) value = nil # If we have a populated attributes_hash - if !@attributes_hash.nil? && @attributes_hash_size > 0 + if @attributes_hash_size > 0 && !@attributes_hash.nil? attribute_metadata = @attributes_hash[member] unless attribute_metadata.nil? value = attribute_metadata.instance_variable_get(:@value_before_type_cast) diff --git a/lib/panko/impl/attributes_writer/active_record/writer.rb b/lib/panko/impl/attributes_writer/active_record/writer.rb index 7aa555e9..da422415 100644 --- a/lib/panko/impl/attributes_writer/active_record/writer.rb +++ b/lib/panko/impl/attributes_writer/active_record/writer.rb @@ -9,12 +9,18 @@ def write_attributes(object, descriptor, writer) attributes_ctx = Context.new(object) object_class = object.class - descriptor.attributes.each do |attribute| + length = descriptor.attributes.length + i = 0 + while i < length + attribute = descriptor.attributes[i] + attribute.invalidate!(object_class) value = attributes_ctx.read_attribute(attribute) ValuesWriter.write(writer, attribute, value) + + i += 1 end end end From 4974f1a7a894df1ac732341436f0d61f39b2ac31 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Fri, 1 Nov 2024 21:34:22 +0000 Subject: [PATCH 4/7] ActiveRecord::Writer no Context allocs --- .../attributes_writer/active_record/writer.rb | 92 ++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/lib/panko/impl/attributes_writer/active_record/writer.rb b/lib/panko/impl/attributes_writer/active_record/writer.rb index da422415..7e7c8d83 100644 --- a/lib/panko/impl/attributes_writer/active_record/writer.rb +++ b/lib/panko/impl/attributes_writer/active_record/writer.rb @@ -5,8 +5,16 @@ module Panko::Impl::AttributesWriter::ActiveRecord class Writer + def initialize + @attributes_hash = EMPTY_HASH + @attributes_hash_size = 0 + @indexed_row_column_indexes = nil + @is_indexed_row = false + @indexed_row_row = nil + end + def write_attributes(object, descriptor, writer) - attributes_ctx = Context.new(object) + set_from_record(object) object_class = object.class length = descriptor.attributes.length @@ -16,12 +24,92 @@ def write_attributes(object, descriptor, writer) attribute.invalidate!(object_class) - value = attributes_ctx.read_attribute(attribute) + value = read_attribute(attribute) ValuesWriter.write(writer, attribute, value) i += 1 end end + + private + + def set_from_record(record) + attributes_set = record._panko_attributes + + attributes_hash = attributes_set._panko_attributes_hash + if attributes_hash&.empty? + @attributes_hash = EMPTY_HASH + @attributes_hash_size = 0 + else + @attributes_hash = attributes_hash + @attributes_hash_size = attributes_hash.size + end + + @types = attributes_set._panko_types + @additional_types = attributes_set._panko_additional_types + @try_to_read_from_additional_types = !@additional_types.nil? && !@additional_types.empty? + + @values = attributes_set._panko_values + + # Check if the values are of type ActiveRecord::Result::IndexedRow + if PANKO_INDEX_ROW_DEFINED && @values.is_a?(ActiveRecord::Result::IndexedRow) + @indexed_row_column_indexes = @values._panko_column_indexes + @indexed_row_row = @values._panko_row + @is_indexed_row = true + else + @indexed_row_column_indexes = nil + @is_indexed_row = false + @indexed_row_row = nil + end + end + + # Reads a value from the indexed row + def read_value_from_indexed_row(member) + return nil if @indexed_row_column_indexes.nil? || @indexed_row_row.nil? + + column_index = @indexed_row_column_indexes[member] + return nil if column_index.nil? + + row = @indexed_row_row + return nil if row.nil? + + row[column_index] + end + + # Reads the attribute value + def read_attribute(attribute) + member = attribute.name + value = nil + + # If we have a populated attributes_hash + if @attributes_hash_size > 0 && !@attributes_hash.nil? + attribute_metadata = @attributes_hash[member] + unless attribute_metadata.nil? + value = attribute_metadata.instance_variable_get(:@value_before_type_cast) + attribute.type ||= attribute_metadata.instance_variable_get(:@type) + end + end + + # Fallback to reading from values or indexed row + if value.nil? && !@values.nil? + value = if @is_indexed_row + read_value_from_indexed_row(member) + else + @values[member] + end + end + + # Fetch the type if not yet set + if attribute.type.nil? && !value.nil? + if @try_to_read_from_additional_types + attribute.type = @additional_types[member] + end + + attribute.type ||= @types[member] + end + + value + end end end From d0f304ccfaca8833c559d53f695c8fef6fb557a6 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Sat, 2 Nov 2024 12:29:50 +0000 Subject: [PATCH 5/7] values_writer/writer: add support for arrays --- .../active_record/values_writer/writer.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb b/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb index 726a3708..d7e1c370 100644 --- a/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb +++ b/lib/panko/impl/attributes_writer/active_record/values_writer/writer.rb @@ -46,7 +46,12 @@ def write(writer, attribute, value) return end - # TODO: validate against arrays + if attribute.type.respond_to?(:subtype) + # TODO: test this. + writer.push_value(attribute.type.deserialize(value), key) + return + end + written = case attribute.type.type when :string, :text, :uuid @string_writer.write(value, writer, key) From 2ef67271a0df6ba2f430f628a56bcd407ea0ed76 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Sat, 2 Nov 2024 14:02:22 +0000 Subject: [PATCH 6/7] time conversion: create utf8 string Oj converts the string to UTF8 which inccurs in performance cost. Given that input string is UTF8, this code should be correct. --- ext/panko_serializer/time_conversion.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/panko_serializer/time_conversion.c b/ext/panko_serializer/time_conversion.c index 7c571aa8..a2bfa37c 100644 --- a/ext/panko_serializer/time_conversion.c +++ b/ext/panko_serializer/time_conversion.c @@ -120,7 +120,7 @@ VALUE iso_ar_iso_datetime_string(const char* value) { } *cur++ = 'Z'; - output = rb_str_new(buf, cur - buf); + output = rb_utf8_str_new(buf, cur - buf); return output; } From d4f6f2be662f6cbf73c2524e154ace31dbbc0ec4 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Fri, 11 Jul 2025 13:38:05 +0300 Subject: [PATCH 7/7] SerializationDescriptor: improve tests --- lib/panko/serializer.rb | 2 +- spec/panko/serialization_descriptor_spec.rb | 418 ++++++++++++++------ 2 files changed, 288 insertions(+), 132 deletions(-) diff --git a/lib/panko/serializer.rb b/lib/panko/serializer.rb index ac053af4..0a828cf7 100644 --- a/lib/panko/serializer.rb +++ b/lib/panko/serializer.rb @@ -123,7 +123,7 @@ def scope @serialization_context.scope end - attr_writer :serialization_context + attr_accessor :serialization_context attr_reader :object def serialize(object) diff --git a/spec/panko/serialization_descriptor_spec.rb b/spec/panko/serialization_descriptor_spec.rb index 0e0c5fc2..d73c084f 100644 --- a/spec/panko/serialization_descriptor_spec.rb +++ b/spec/panko/serialization_descriptor_spec.rb @@ -3,215 +3,371 @@ require "spec_helper" describe Panko::SerializationDescriptor do - class FooSerializer < Panko::Serializer - attributes :name, :address - end + describe "attributes" do + subject { described_class.build(FooSerializer) } - context "attributes" do - it "simple fields" do - descriptor = Panko::SerializationDescriptor.build(FooSerializer) + class FooSerializer < Panko::Serializer + attributes :name, :address + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([ + it "returns all attributes" do + expect(subject.attributes).to match_array([ Panko::Attribute.create(:name), Panko::Attribute.create(:address) ]) end - it "method attributes" do + it "returns no method fields for FooSerializer" do + expect(subject.method_fields).to be_empty + end + + context "with method attributes" do class SerializerWithMethodsSerializer < Panko::Serializer attributes :name, :address, :something - def something "#{object.name} #{object.address}" end end + let(:descriptor) { described_class.build(SerializerWithMethodsSerializer) } - descriptor = Panko::SerializationDescriptor.build(SerializerWithMethodsSerializer) + it "returns attributes" do + expect(descriptor.attributes).to match_array([ + Panko::Attribute.create(:name), + Panko::Attribute.create(:address) + ]) + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([ - Panko::Attribute.create(:name), - Panko::Attribute.create(:address) - ]) - expect(descriptor.method_fields).to eq([:something]) + it "returns method fields" do + expect(descriptor.method_fields).to contain_exactly(Panko::Attribute.create(:something)) + end end - it "aliases" do + context "with aliased attributes" do class AttribteAliasesSerializer < Panko::Serializer aliases name: :full_name end + let(:descriptor) { described_class.build(AttribteAliasesSerializer) } - descriptor = Panko::SerializationDescriptor.build(AttribteAliasesSerializer) - - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([ - Panko::Attribute.create(:name, alias_name: :full_name) - ]) - end - - it "allows multiple filters in other runs" do - class MultipleFiltersTestSerializer < Panko::Serializer - attributes :name, :address - has_many :foos, each_serializer: FooSerializer + it "returns aliased attributes" do + expect(descriptor.attributes).to contain_exactly( + Panko::Attribute.create(:name, alias_name: :full_name) + ) end - - descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer, only: { - instance: [:foos], - foos: [:name] - }) - - expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([ - Panko::Attribute.create(:name) - ]) - - descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer) - - expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([ - Panko::Attribute.create(:name), - Panko::Attribute.create(:address) - ]) end end - context "associations" do - it "has_one: build_descriptor" do + describe "associations" do + context "has_one association" do class BuilderTestFooHolderHasOneSerializer < Panko::Serializer attributes :name - has_one :foo, serializer: FooSerializer end - descriptor = Panko::SerializationDescriptor.build(BuilderTestFooHolderHasOneSerializer) + class FooSerializer < Panko::Serializer + attributes :name, :address + end + let(:descriptor) { described_class.build(BuilderTestFooHolderHasOneSerializer) } - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)]) - expect(descriptor.method_fields).to be_empty + it "returns attributes" do + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + end - expect(descriptor.has_one_associations.count).to eq(1) + it "returns no method fields" do + expect(descriptor.method_fields).to be_empty + end - foo_association = descriptor.has_one_associations.first - expect(foo_association.name_sym).to eq(:foo) + it "returns one has_one association" do + expect(descriptor.has_one_associations.size).to eq(1) + end - foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {}) - expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes) - expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields) + it "returns correct has_one association details" do + foo_association = descriptor.has_one_associations.first + expect(foo_association.name_sym).to eq(:foo) + foo_descriptor = described_class.build(FooSerializer, {}) + expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes) + expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields) + end end - it "has_many: builds descriptor" do + context "has_many association" do class BuilderTestFoosHasManyHolderSerializer < Panko::Serializer attributes :name - has_many :foos, serializer: FooSerializer end - descriptor = Panko::SerializationDescriptor.build(BuilderTestFoosHasManyHolderSerializer) + class FooSerializer < Panko::Serializer + attributes :name, :address + end + let(:descriptor) { described_class.build(BuilderTestFoosHasManyHolderSerializer) } + + it "returns attributes" do + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)]) - expect(descriptor.method_fields).to be_empty - expect(descriptor.has_one_associations).to be_empty + it "returns no method fields" do + expect(descriptor.method_fields).to be_empty + end - expect(descriptor.has_many_associations.count).to eq(1) + it "returns no has_one associations" do + expect(descriptor.has_one_associations).to be_empty + end - foo_association = descriptor.has_many_associations.first - expect(foo_association.name_sym).to eq(:foos) + it "returns one has_many association" do + expect(descriptor.has_many_associations.size).to eq(1) + end - foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {}) - expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes) - expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields) + it "returns correct has_many association details" do + foo_association = descriptor.has_many_associations.first + expect(foo_association.name_sym).to eq(:foos) + foo_descriptor = described_class.build(FooSerializer, {}) + expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes) + expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields) + end end end - context "filter" do - it "only" do - descriptor = Panko::SerializationDescriptor.build(FooSerializer, only: [:name]) + describe "filtering" do + context "attribute filtering" do + it "filters attributes with only" do + class FooSerializer < Panko::Serializer + attributes :name, :address + end + descriptor = described_class.build(FooSerializer, only: [:name]) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + expect(descriptor.method_fields).to be_empty + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)]) - expect(descriptor.method_fields).to be_empty - end + it "filters attributes with except" do + class FooSerializer < Panko::Serializer + attributes :name, :address + end + descriptor = described_class.build(FooSerializer, except: [:name]) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:address)) + expect(descriptor.method_fields).to be_empty + end - it "except" do - descriptor = Panko::SerializationDescriptor.build(FooSerializer, except: [:name]) + context "with aliases" do + class ExceptFooWithAliasesSerializer < Panko::Serializer + aliases name: :full_name, address: :full_address + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:address)]) - expect(descriptor.method_fields).to be_empty - end + class OnlyFooWithAliasesSerializer < Panko::Serializer + attributes :address + aliases name: :full_name + end - it "except filters aliases" do - class ExceptFooWithAliasesSerializer < Panko::Serializer - aliases name: :full_name, address: :full_address - end + class OnlyWithFieldsFooWithAliasesSerializer < Panko::Serializer + attributes :address, :another_field + aliases name: :full_name + end + + it "filters aliases with except" do + descriptor = described_class.build(ExceptFooWithAliasesSerializer, except: [:full_name]) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:address, alias_name: :full_address)) + end - descriptor = Panko::SerializationDescriptor.build(ExceptFooWithAliasesSerializer, except: [:full_name]) + it "filters aliases with only" do + descriptor = described_class.build(OnlyFooWithAliasesSerializer, only: [:full_name]) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name, alias_name: :full_name)) + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:address, alias_name: :full_address)]) + it "filters aliases and fields with only" do + descriptor = described_class.build(OnlyWithFieldsFooWithAliasesSerializer, only: %i[full_name address]) + expect(descriptor.attributes).to match_array([ + Panko::Attribute.create(:address), + Panko::Attribute.create(:name, alias_name: :full_name) + ]) + end + end end - it "only filters aliases" do - class OnlyFooWithAliasesSerializer < Panko::Serializer - attributes :address - aliases name: :full_name + context "association filtering" do + class FooSerializer < Panko::Serializer + attributes :name, :address + end + + it "filters has_one associations with only" do + class FooHasOneSerilizers < Panko::Serializer + attributes :name + has_one :foo1, serializer: FooSerializer + has_one :foo2, serializer: FooSerializer + end + descriptor = described_class.build(FooHasOneSerilizers, only: %i[name foo1]) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + expect(descriptor.has_one_associations.size).to eq(1) + foos_assoc = descriptor.has_one_associations.first + expect(foos_assoc.name_sym).to eq(:foo1) + expect(foos_assoc.descriptor.attributes).to match_array([ + Panko::Attribute.create(:name), + Panko::Attribute.create(:address) + ]) end - descriptor = Panko::SerializationDescriptor.build(OnlyFooWithAliasesSerializer, only: [:full_name]) + it "filters has_many associations with nested only" do + class AssocFilterTestFoosHolderSerializer < Panko::Serializer + attributes :name + has_many :foos, serializer: FooSerializer + end + descriptor = described_class.build(AssocFilterTestFoosHolderSerializer, only: {foos: [:address]}) + expect(descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + expect(descriptor.has_many_associations.size).to eq(1) + foos_assoc = descriptor.has_many_associations.first + expect(foos_assoc.name_sym).to eq(:foos) + expect(foos_assoc.descriptor.attributes).to contain_exactly(Panko::Attribute.create(:address)) + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name, alias_name: :full_name)]) + context "deep filtering" do + class DeepFooSerializer < Panko::Serializer + attributes :name, :address + has_one :child, serializer: FooSerializer + has_many :items, serializer: FooSerializer + end + + it "applies deep only filters" do + descriptor = described_class.build(DeepFooSerializer, only: { + instance: [:name, :child, :items], + child: [:address], + items: [:name] + }) + expect(descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:name) + expect(descriptor.has_one_associations.first.descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:address) + expect(descriptor.has_many_associations.first.descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:name) + end + + it "applies deep except filters" do + descriptor = described_class.build(DeepFooSerializer, except: { + instance: [:name], + child: [:address], + items: [:name] + }) + expect(descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:address) + expect(descriptor.has_one_associations.first.descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:name) + expect(descriptor.has_many_associations.first.descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:address) + end + + it "handles conflicting filters" do + descriptor = described_class.build(DeepFooSerializer, + only: {instance: [:name, :address]}, + except: {instance: [:address]}) + expect(descriptor.attributes.map(&:name).map(&:to_sym)).to contain_exactly(:name) + end + end end - it "only - filters aliases and fields" do - class OnlyWithFieldsFooWithAliasesSerializer < Panko::Serializer - attributes :address, :another_field - aliases name: :full_name + context "filter persistence" do + class FooSerializer < Panko::Serializer + attributes :name, :address end - descriptor = Panko::SerializationDescriptor.build(OnlyWithFieldsFooWithAliasesSerializer, only: %i[full_name address]) + class MultipleFiltersTestSerializer < Panko::Serializer + attributes :name, :address + has_many :foos, each_serializer: FooSerializer + end - expect(descriptor).not_to be_nil - expect(descriptor.attributes).to eq([ - Panko::Attribute.create(:address), - Panko::Attribute.create(:name, alias_name: :full_name) - ]) + it "does not persist filters between runs" do + descriptor = described_class.build(MultipleFiltersTestSerializer, only: { + instance: [:foos], + foos: [:name] + }) + descriptor2 = described_class.build(MultipleFiltersTestSerializer) + expect(descriptor.has_many_associations.first.descriptor.attributes).to contain_exactly(Panko::Attribute.create(:name)) + expect(descriptor2.has_many_associations.first.descriptor.attributes).to match_array([ + Panko::Attribute.create(:name), + Panko::Attribute.create(:address) + ]) + end end - it "filters associations" do - class FooHasOneSerilizers < Panko::Serializer - attributes :name + describe "filter resolution" do + let(:descriptor) { described_class.new } - has_one :foo1, serializer: FooSerializer - has_one :foo2, serializer: FooSerializer + it "handles empty filters" do + attrs, assocs = descriptor.resolve_filters({}, :only) + expect(attrs).to be_empty + expect(assocs).to eq({}) end - descriptor = Panko::SerializationDescriptor.build(FooHasOneSerilizers, only: %i[name foo1]) + it "handles array filters" do + attrs, assocs = descriptor.resolve_filters({only: [:name, :address]}, :only) + expect(attrs).to match_array([:name, :address]) + expect(assocs).to eq({}) + end - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)]) - expect(descriptor.has_one_associations.count).to eq(1) + it "handles hash filters" do + filters = { + only: { + instance: [:name], + association: [:address] + } + } + attrs, assocs = descriptor.resolve_filters(filters, :only) + expect(attrs).to match_array([:name]) + expect(assocs).to eq({association: [:address]}) + end + end + end - foos_assoc = descriptor.has_one_associations.first - expect(foos_assoc.name_sym).to eq(:foo1) - expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:name), Panko::Attribute.create(:address)]) + describe "context handling" do + it "sets serialization context" do + class ContextTestSerializer < Panko::Serializer + attributes :name + def name + serialization_context&.custom_name || object.name + end + end + context = OpenStruct.new(custom_name: "CustomName") + descriptor = described_class.build(ContextTestSerializer, {}, context) + expect(descriptor.serializer.serialization_context).to eq(context) end - describe "association filters" do - it "accepts only as option" do - class AssocFilterTestFoosHolderSerializer < Panko::Serializer - attributes :name - has_many :foos, serializer: FooSerializer + it "propagates context to associations" do + class ContextTestSerializer < Panko::Serializer + attributes :name + def name + serialization_context&.custom_name || object.name end + end - descriptor = Panko::SerializationDescriptor.build(AssocFilterTestFoosHolderSerializer, only: {foos: [:address]}) + class ContextWithAssociationsSerializer < Panko::Serializer + has_one :foo, serializer: ContextTestSerializer + has_many :bars, serializer: ContextTestSerializer + end + context = OpenStruct.new(custom_name: "CustomName") + descriptor = described_class.build(ContextWithAssociationsSerializer, {}, context) + expect(descriptor.has_one_associations.first.descriptor.serializer.serialization_context).to eq(context) + expect(descriptor.has_many_associations.first.descriptor.serializer.serialization_context).to eq(context) + end + end - expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)]) - expect(descriptor.has_many_associations.count).to eq(1) + describe "duplicate behavior" do + class FooSerializer < Panko::Serializer + attributes :name, :address + end - foos_assoc = descriptor.has_many_associations.first - expect(foos_assoc.name_sym).to eq(:foos) - expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:address)]) + class DuplicateTestSerializer < Panko::Serializer + attributes :name, :address + def custom_method + "test" end + has_one :foo, serializer: FooSerializer + has_many :bars, serializer: FooSerializer + end + + let(:original) { described_class.build(DuplicateTestSerializer) } + let(:duplicate) { described_class.duplicate(original) } + + it "creates independent copy of attributes and associations" do + expect(duplicate.attributes.object_id).not_to eq(original.attributes.object_id) + expect(duplicate.method_fields.object_id).not_to eq(original.method_fields.object_id) + expect(duplicate.has_one_associations.first.object_id).not_to eq(original.has_one_associations.first.object_id) + expect(duplicate.has_many_associations.first.object_id).not_to eq(original.has_many_associations.first.object_id) + end + + it "maintains same values after duplication" do + expect(duplicate.attributes).to eq(original.attributes) + expect(duplicate.method_fields).to eq(original.method_fields) + expect(duplicate.has_one_associations.map(&:name_sym)).to eq(original.has_one_associations.map(&:name_sym)) + expect(duplicate.has_many_associations.map(&:name_sym)).to eq(original.has_many_associations.map(&:name_sym)) end end end