Skip to content

Fixed the messy auto-generated serde logic in the library #29

@mridang

Description

@mridang

Title: Refactor Ruby SDK Models with Shale Mapper via ZitadelModel Base Class

Description:
This initiative will refactor the Ruby SDK's model classes to leverage the Shale gem for object mapping and JSON serialization/deserialization. The goal is to significantly reduce boilerplate, improve maintainability, and adopt a more standard Ruby approach by introducing a common ZitadelModel base class and updating model generation templates.

Problem:

The current Ruby models, generated by OpenAPI Generator, include substantial duplicated code and rely on conventions like attribute_map, openapi_types, build_from_hash, to_hash, and helper methods in ApiClient for serialization/deserialization. This leads to:

  • Increased maintenance overhead for models.
  • Less idiomatic Ruby compared to modern mapping libraries.
  • Potential inconsistencies in serialization logic.

Impact:

Refactoring will result in:

  • Slimmer, cleaner, and more maintainable model classes using Shale's DSL.
  • Standardized and robust JSON handling powered by the Shale gem.
  • Reduced risk of bugs associated with custom serialization logic.
  • Improved developer experience when working with SDK models.

Solution / Tasks:

1. Implement ZitadelModel Base Class:
Create a module ZitadelModel or class ZitadelModel that includes Shale::Mapper. This base will provide common JSON and Hash serialization/deserialization methods for all SDK models to inherit or include. Since Shale's mapping is defined in the inheriting class, the base might primarily provide convenience methods wrapping Shale's core functionality.

Example ZitadelModel.rb (using a base class approach):

require 'shale'

module Zitadel
  module Client # Or your chosen SDK namespace
    class ZitadelModel < Shale::Mapper
      # Shale::Mapper provides from_json, to_json, from_hash, to_hash
      # We can add aliases or wrappers if needed, but often direct usage is fine.

      # Example wrapper for consistency if desired:
      def to_zitadel_hash(mode: :openapi)
        # Shale's to_hash mode :openapi might be relevant if defined
        # Default to_hash produces string keys suitable for JSON
        to_hash
      end

      def to_zitadel_json(**options)
        to_json(**options) # Pass options like :pretty
      end

      def self.from_zitadel_hash(hash_data)
        from_hash(hash_data)
      end

      def self.from_zitadel_json(json_string)
        from_json(json_string)
      end
    end
  end
end

2. Update OpenAPI Generator Templates for Ruby Models:
Modify the OpenAPI Generator templates for Ruby models to:

  • Make generated models inherit from ZitadelModel (or include the module).
  • Define attributes using Shale's attribute DSL (e.g., attribute :name, Shale::Type::String, attribute :details, UserServiceDetailsMapper).
  • Use Shale's json { map 'jsonKey', to: :ruby_attribute } block for mapping differing JSON keys to Ruby attribute names.
  • Remove the old boilerplate code (static attribute_map, openapi_types, build_from_hash, _deserialize, to_hash, _to_hash, etc.).
  • Ensure enums are handled as simple string/integer properties or custom Shale types if needed.
  • Handle default values using Shale's default: -> { ... } option on attributes.

Target structure for a generated model (e.g., user_service_user.rb):

require 'shale'
# Assuming other models (UserServiceDetailsMapper etc.) are also defined using Shale
# and inherit from ZitadelModel

module Zitadel
  module Client
    # Assuming UserServiceUserState is handled, e.g., as Shale::Type::String
    class UserServiceUser < ZitadelModel
      attribute :user_id, Shale::Type::String
      attribute :details, UserServiceDetailsMapper, default: -> { nil }
      attribute :state, Shale::Type::String # Or custom type/enum handling
      attribute :username, Shale::Type::String
      attribute :login_names, Shale::Type::Array.of(Shale::Type::String), default: -> { [] }
      attribute :preferred_login_name, Shale::Type::String, default: -> { nil }
      attribute :human, UserServiceHumanUserMapper, default: -> { nil }
      attribute :machine, UserServiceMachineUserMapper, default: -> { nil }

      json do
        map 'userId', to: :user_id
        map 'details', to: :details
        map 'state', to: :state
        map 'username', to: :username
        map 'loginNames', to: :login_names
        map 'preferredLoginName', to: :preferred_login_name
        map 'human', to: :human
        map 'machine', to: :machine
      end

      # The initialize method is provided by Shale::Mapper
      # to_hash, from_hash, to_json, from_json are inherited/provided by Shale::Mapper
    end
  end
end

3. Update SDK Code to Use New Model Methods:

  • Search the SDK codebase (likely within ApiClient or similar) for usages of the old deserialization logic (ApiClient#deserialize, ApiClient#convert_to_type, Model.build_from_hash).
  • Replace these calls with ModelClass.from_json(response_body_string) or ModelClass.from_hash(parsed_hash).
  • Search for usages of the old serialization logic (ApiClient#object_to_http_body, instance.to_hash).
  • Replace these calls with $instance.to_json() (if a JSON string is needed for the request body) or $instance.to_hash() (if a hash is needed).
  • The aim is to eliminate the models' dependency on the old custom serialization/deserialization helpers.

Expected Outcomes:

  • Ruby SDK models are significantly leaner, using Shale's DSL for definition and inheriting ser/des logic.
  • JSON serialization and deserialization are handled robustly by the Shale gem.
  • The custom serialization/deserialization logic within ApiClient and the old model boilerplate is removed.
  • The SDK is easier to maintain and aligns better with modern Ruby practices.
  • Functional equivalence for JSON-based API interactions is preserved.

Additional Notes:

  • Dependencies: Ensure Gemfile includes gem 'shale'.
  • Null Handling: Review Shale's handling of nil values during serialization (it generally includes explicit nulls if the attribute is nil in Ruby). Configure as needed if omission is preferred.
  • Testing: Thorough testing of serialization/deserialization for various model types (including nested objects, arrays, dates, etc.) is essential.
  • Discriminators: Functionality related to discriminators is explicitly out of scope for this task.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions