Class: McpToolkit::Serializer::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_toolkit/serializer/base.rb

Overview

The DEFAULT serializer base shipped by the toolkit. A self-contained implementation of the subset of an AMS-style serializer the MCP wire format depends on, with NO dependency on active_model_serializers / fast_jsonapi.

The injection contract

The executors (ListExecutor / GetExecutor) only ever call two class methods on a resource's serializer:

serializer.serialize_one(record, scope:)
# => Hash (a single record's shape), or nil for a nil record

serializer.serialize_collection(records, scope:, total_count:, limit:, offset:)
# => { <root_key> => [ <record_hash>, ... ],
#      meta: { total_count:, limit:, offset: } }

ANY class implementing those two methods can be registered as a resource's serializer — that is the seam that lets an app's existing serializers slot in unchanged alongside this base. The resource_schema tool additionally reads declared_attributes and declared_associations off the serializer (for shape discovery); a custom serializer that wants to power resource_schema should expose those too, but they are not required for get / list.

Sparse fieldsets (optional)

Both entry points also accept an OPTIONAL fields: keyword — an array of the attribute and/or relationship link-key names to include (JSON:API's sparse fieldset; the two share one flat namespace). fields: nil (the default) means "everything", so the contract above is unchanged for callers that omit it. When a subset is given, only those members are emitted AND only the selected relationships are loaded (unselected has_many links are never queried). A serializer that does NOT declare a fields: keyword still supports sparse fieldsets — McpToolkit::Serialization prunes its output instead — so honoring fields: natively is a performance optimization, not a contract requirement.

scope is whatever the serializer needs (typically the account); it may be nil for models without translations.

Output shape

A single record serializes to:

{ <attr> => <value>, ..., "links" => { "<assoc>" => <id|[ids]|{id:,type:}|nil> } }
  • Declared attributes are emitted as symbol keys, in declaration order (an instance method named after the attribute overrides the column value).
  • "links" is a string key whose value is a Hash with string keys, one per declared association, sorted alphabetically.
    • has_one / belongs_to whose FK lives on the record => the raw id (or nil)
    • polymorphic has_one / belongs_to => { id: , type: }
    • has_many => a sorted Array of associated ids ([] when none)
  • created_at / updated_at, when present, are rendered as iso8601(6).

A collection serializes to:

{ <plural_resource_name>: [ <record_hash>, ... ],
meta: { total_count:, limit:, offset: } }

Defined Under Namespace

Classes: Association

Constant Summary collapse

TIMESTAMP_COLUMNS =
%i[created_at updated_at].freeze
HIGH_PRECISION_FOR_TIMESTAMPS =
6

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(object, scope: nil) ⇒ Base

Returns a new instance of Base.



172
173
174
175
# File 'lib/mcp_toolkit/serializer/base.rb', line 172

def initialize(object, scope: nil)
  @object = object
  @scope = scope
end

Instance Attribute Details

#objectObject (readonly)

---- instance API ----------------------------------------------------



170
171
172
# File 'lib/mcp_toolkit/serializer/base.rb', line 170

def object
  @object
end

#scopeObject (readonly)

---- instance API ----------------------------------------------------



170
171
172
# File 'lib/mcp_toolkit/serializer/base.rb', line 170

def scope
  @scope
end

Class Method Details

.attributes(*names) ⇒ Object



74
75
76
# File 'lib/mcp_toolkit/serializer/base.rb', line 74

def self.attributes(*names)
  names.each { |name| declared_attributes << name.to_sym }
end

.declared_associationsObject



113
114
115
# File 'lib/mcp_toolkit/serializer/base.rb', line 113

def self.declared_associations
  @declared_associations ||= []
end

.declared_attributesObject



109
110
111
# File 'lib/mcp_toolkit/serializer/base.rb', line 109

def self.declared_attributes
  @declared_attributes ||= []
end

.has_many(name, key: nil, root: nil, serializer: nil) ⇒ Object

has_many / has_and_belongs_to_many - sorted array of ids.



92
93
94
95
96
# File 'lib/mcp_toolkit/serializer/base.rb', line 92

def self.has_many(name, key: nil, root: nil, serializer: nil)
  declared_associations << Association.new(
    name: name.to_sym, type: :has_many, key: key || root, serializer:, polymorphic: false
  )
end

.has_one(name, key: nil, root: nil, serializer: nil, polymorphic: false, foreign_key: nil) ⇒ Object

belongs_to / has_one - single id (or id:,type: when polymorphic).

foreign_key: overrides the FK method read for the id (defaults to <name>_id). Use it when the model's FK column doesn't follow the <name>_id convention - e.g. has_one :account, foreign_key: :synced_account_id so the link reports the central account id straight off the already-loaded column.



85
86
87
88
89
# File 'lib/mcp_toolkit/serializer/base.rb', line 85

def self.has_one(name, key: nil, root: nil, serializer: nil, polymorphic: false, foreign_key: nil)
  declared_associations << Association.new(
    name: name.to_sym, type: :has_one, key: key || root, serializer:, polymorphic:, foreign_key:
  )
end

.model_classObject

Infer the serialized model from the serializer class name by stripping a trailing "Serializer" and the host namespace, e.g. Mcp::NotificationSerializer -> Notification Mcp::PushNotifications::FilterSerializer -> PushNotifications::Filter Subclasses whose name doesn't follow the convention set model_class.



150
151
152
153
154
155
156
157
158
# File 'lib/mcp_toolkit/serializer/base.rb', line 150

def self.model_class
  @model_class ||= begin
    without_suffix = name.delete_suffix("Serializer")
    # Drop the leading serializer namespace segment (e.g. "Mcp::") so the
    # remainder names the model. If there is no namespace, use as-is.
    without_namespace = without_suffix.sub(/\A[^:]+::/, "")
    (without_namespace.empty? ? without_suffix : without_namespace).constantize
  end
end

.model_class=(klass) ⇒ Object

Lets subclasses point at a model whose name doesn't follow the convention (e.g. namespacing differences). Written as an explicit class method (not attr_writer, which would define an instance writer) to set the class-level @model_class the convention-inference memoizes.



164
165
166
# File 'lib/mcp_toolkit/serializer/base.rb', line 164

def self.model_class=(klass) # rubocop:disable Style/TrivialAccessors
  @model_class = klass
end

.root_keyObject

Pluralized resource name used as the collection root key, derived from the serialized model (model.model_name.plural).



141
142
143
# File 'lib/mcp_toolkit/serializer/base.rb', line 141

def self.root_key
  model_class.model_name.plural.to_sym
end

.serialize_collection(records, scope: nil, total_count: nil, limit: nil, offset: nil, fields: nil) ⇒ Object

Serialize an array of records to the index wrapper, keyed by the pluralized resource name, with a meta pagination block. fields: (optional) restricts every row to a sparse fieldset — see the class-level docs.



131
132
133
134
135
136
137
# File 'lib/mcp_toolkit/serializer/base.rb', line 131

def self.serialize_collection(records, scope: nil, total_count: nil, limit: nil, offset: nil, fields: nil)
  rows = Array(records).map { |record| new(record, scope:).serializable_hash(fields:) }
  {
    root_key => rows,
    meta: { total_count: total_count.nil? ? rows.size : total_count, limit:, offset: }
  }
end

.serialize_one(record, scope: nil, fields: nil) ⇒ Object

Serialize a single record to its attributes+links hash. nil-safe. fields: (optional) restricts the output to a sparse fieldset — see the class-level docs.



122
123
124
125
126
# File 'lib/mcp_toolkit/serializer/base.rb', line 122

def self.serialize_one(record, scope: nil, fields: nil)
  return nil if record.nil?

  new(record, scope:).serializable_hash(fields:)
end

.translates(*names) ⇒ Object

Declares attributes whose value is a { locale => translation } hash. An instance method is defined for each attribute that delegates to #translate. Only meaningful for Globalize models; harmless otherwise (returns {}).



102
103
104
105
106
107
# File 'lib/mcp_toolkit/serializer/base.rb', line 102

def self.translates(*names)
  names.each do |name|
    declared_attributes << name.to_sym unless declared_attributes.include?(name.to_sym)
    define_method(name) { translate(name) }
  end
end

Instance Method Details

#serializable_hash(fields: nil) ⇒ Object Also known as: as_json

fields: (optional) is a sparse fieldset — an array of attribute and/or relationship link-key names to include. nil (default) emits everything.



179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/mcp_toolkit/serializer/base.rb', line 179

def serializable_hash(fields: nil)
  selected = fields&.map(&:to_sym)
  hash = {}
  self.class.declared_attributes.each do |attr|
    next if selected && !selected.include?(attr)

    hash[attr] = read_attribute(attr)
  end
  apply_high_precision_timestamps(hash)
  link_hash = links(selected)
  hash["links"] = link_hash unless link_hash.nil?
  hash
end