Module: ActivemodelObjectInfo::Base

Defined in:
lib/activemodel_object_info/base.rb

Overview

通用的设置操作项相关的模型方法和处理。

核心作用与职责: 提供模型实例数据格式化输出能力。主要用于将 ActiveRecord 或 ActiveModel 的实例安全、灵活地转换为 Hash, 方便后续转化为 JSON 等格式输出给前端,支持字段过滤、重命名、自定义处理及日期格式化。

核心重要方法:

[Changelog] [2026-07-28] 支持关联对象嵌套输出 (includes) 与场景化输出配置 (context) (shiner527) [2026-07-28] 补充完整 YARD 文档及核心注释,重构 instance_info 签名以兼容 Ruby 3.x 关键字参数 (shiner527) [2021-04-19] 创建基础模块,提供 instance_info 数据格式化输出功能 (shiner527)

Examples:

详细使用方法与示例

class User
  include ActivemodelObjectInfo::Base
  attr_accessor :id, :name, :created_at

  INSTANCE_INFO = {
    only: [:id, :name, :created_at],
    attributes: [
      :id,
      { name: :name, as: :user_name },
      { name: :created_at, format: :date }
    ]
  }.freeze
end

user = User.new(id: 1, name: "test", created_at: Time.now)
user.instance_info # => { id: 1, user_name: "test", created_at: "2026-07-28" }

Constant Summary collapse

DATETIME_FULL =

满格式

'%Y-%m-%d %H:%M:%S'
DATETIME_MIN =

截止到分

'%Y-%m-%d %H:%M'
DATETIME_DATE =

仅日期

'%Y-%m-%d'
DATETIME_MONTH =

仅年月

'%Y-%m'
DATETIME_YEAR =

仅年份

'%Y'

Instance Method Summary collapse

Instance Method Details

#instance_info(options_hash = nil, **keyword_options) ⇒ Hash

对象信息输出。主要返回给前端一个可用的散列(会被转化为JSON格式)格式的信息并传递给前端。

Parameters:

  • options_hash (Hash) (defaults to: nil)

    设置选项(传统位置参数 Hash),如果传入将被与 keyword_options 合并

  • keyword_options (Hash)

    设置选项(Ruby 3 关键字参数)

Options Hash (**keyword_options):

  • :context (Symbol, String)

    场景上下文名称。如果提供,将优先读取 "INSTANCE_INFO_#ActivemodelObjectInfo::Base.contextcontext.upcase" 常量。

  • :attributes (Array<Symbol, Hash>)

    具体每一项输出的设置数组。每个元素既可以是标识符实例也可以是一个散列实例。 如果是标识符实例,则表示输出该属性。如果是一个散列实例,则按照散列中的设定值去生成对应的内容。

  • :only (Array<Symbol>)

    给出具体可以用来输出的字段属性名数组。

  • :except (Array<Symbol>)

    给出需要被排除输出的字段属性名数组。

  • :datetime_format (String, Symbol)

    全局的时间格式设置。

  • :includes (Hash)

    需要嵌套输出的关联对象配置(如 { profile: { only: [:avatar] } })。

Returns:

  • (Hash)

    返回的处理过的该对象的信息散列。



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/activemodel_object_info/base.rb', line 70

def instance_info(options_hash = nil, **keyword_options)
  # 合并传统参数与关键字参数,完美兼容 Ruby 2.x 的 Hash 传参和 Ruby 3.x 的 **kwargs 传参
  options = (options_hash || {}).merge(keyword_options)

  # 尝试获取当前类上配置的常量作为默认选项
  # 优先级:传入的参数 options > 场景常量 INSTANCE_INFO_#{CONTEXT} > 默认常量 INSTANCE_INFO
  if options.blank? || (options.keys.map(&:to_sym) == [:context] && options[:context].present?)
    context_name = options[:context].to_s.upcase

    # 尝试寻找带场景的常量
    context_constant_name = "#{self.class}::INSTANCE_INFO_#{context_name}"
    options = context_constant_name.safe_constantize if context_name.present?

    # 如果带场景的常量不存在,或者没传场景,降级寻找默认常量
    options ||= "#{self.class}::INSTANCE_INFO".safe_constantize || {}
  end

  # 将 options 的 key 转为 symbol,避免传入字符串 key 导致匹配不到
  options = options.deep_symbolize_keys

  result = {}

  # 1. 整理包含字段和排除字段
  only_attributes = (options[:only] || []).map(&:to_sym)

  # 默认排除逻辑:如果不特意指定,系统会默认排除 deleted 等敏感字段,防止软删除数据泄漏
  default_deleted_column = ::Constants::Base::TABLE_COLUMN_DELETE_COLUMN rescue 'deleted'
  default_except_attrs = [
    default_deleted_column,
    "#{default_deleted_column}_by",
    "#{default_deleted_column}_at",
  ]
  except_attributes = (options[:except] || default_except_attrs).map(&:to_sym)

  # 2. 从当前类所有属性中计算出最终要输出的属性名集合
  attribute_configs = options[:attributes] || []
  output_attributes = attribute_names.map(&:to_sym)
  output_attributes &= only_attributes if only_attributes.present?
  output_attributes -= except_attributes

  # 3. 遍历并处理属性配置(attribute_configs)
  # 这里保证了要引入的类中含有 attributes 方法,且为 Hash 类型

  # 补丁:如果传入了 only_attributes,但是 options[:attributes] 是空(即调用方没传),
  # 我们需要将 only_attributes 中的字段视为基础配置,以确保只包含这几个字段的正常输出
  attribute_configs = only_attributes.map { |attr| { name: attr } } if attribute_configs.empty? && only_attributes.present?

  attribute_configs.each do |attr_config|
    # 根据配置项的类型(Symbol/String 或 Hash)提取字段名称和具体配置
    if [::String, ::Symbol].any? { |attr_class| attr_config.is_a?(attr_class) }
      attribute_name = attr_config.to_sym
      current_attr_config = {}
    elsif attr_config.is_a?(::Hash)
      current_attr_config = attr_config.deep_symbolize_keys
      attribute_name = current_attr_config[:name]
    else
      # 如果配置格式非法则直接跳过
      next
    end

    # 确定实际取值的属性名,支持通过 as 别名取值
    raw_name = current_attr_config[:as].present? ? current_attr_config[:as].to_sym : attribute_name
    filter = current_attr_config[:filter]

    # 取出对象中的实际值
    k = raw_name
    # 特殊处理 abstract 类型:当类型为抽象类时,由于可能没有对应的真实属性方法,直接赋值为 nil,后续依赖 filter 来生成实际值
    v = current_attr_config[:type] == :abstract ? nil : __send__(k)

    # 跳过条件:既不在最终输出名单中,也不是显式声明的 abstract/method 虚拟字段
    next unless output_attributes.include?(k) || %i[abstract method].include?(current_attr_config[:type])

    # 4. 根据类型或 filter 对值进行格式化转换
    if filter.present?
      # 如果配置了 filter,通过 Proc、Symbol(调用同名方法) 或 直接常量 的形式进行过滤转换
      result[attribute_name] = case filter
                               when ::Proc
                                 instance_exec(v, &filter)
                               when ::Symbol
                                 __send__(filter)
                               else
                                 filter
                               end
    elsif [::Date, ::Time, ::DateTime].any? { |time_class| v.is_a?(time_class) }
      # 对时间类型的字段进行格式化,优先级:字段级自定义 format > 全局 datetime_format > 默认全格式
      attribute_format = current_attr_config[:format].present? ? current_attr_config[:format] : options[:datetime_format]
      result[attribute_name] = if attribute_format.to_s == 'standard'
                                 v # standard 保持时间对象原生格式
                               elsif attribute_format.present?
                                 # 匹配内置的别名格式 (full/min/date/month/year),否则作为自定义 strftime 字符串处理
                                 if %i[full min date month year].include?(attribute_format.to_sym)
                                   v.strftime("::#{self.class}::DATETIME_#{attribute_format.to_s.upcase}".constantize)
                                 else
                                   v.strftime(attribute_format)
                                 end
                               else
                                 # 缺省转换为 '%Y-%m-%d %H:%M:%S'
                                 v.strftime(DATETIME_FULL)
                               end
    else
      # 普通值直接赋值
      result[attribute_name] = v
    end
  end

  # 统一将结果第一层的键名转化为 Symbol 格式
  result.symbolize_keys!

  # 5. 处理嵌套关联对象 (includes)
  format_associations!(result, options)

  result
end