lib/alba.rb
require 'json'
require_relative 'alba/version'
require_relative 'alba/errors'
require_relative 'alba/resource'
require_relative 'alba/deprecation'
require_relative 'alba/railtie' if defined?(Rails::Railtie)
# Core module
module Alba
class << self
attr_reader :backend, :encoder
# Getter for inflector, a module responsible for inflecting strings
attr_reader :inflector
# Set the backend, which actually serializes object into JSON
#
# @param backend [#to_sym, nil] the name of the backend
# Possible values are `oj`, `active_support`, `default`, `json` and nil
# @return [Proc] the proc to encode object into JSON
# @raise [Alba::UnsupportedBackend] if backend is not supported
def backend=(backend)
@backend = backend&.to_sym
set_encoder_from_backend
end
# Set encoder, a Proc object that accepts an object and generates JSON from it
# Set backend as `:custom` which indicates no preset encoder is used
#
# @param encoder [Proc]
# @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
def encoder=(encoder)
raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
@encoder = encoder
@backend = :custom
end
# Serialize the object with inline definitions
#
# @param object [Object] the object to be serialized
# @param root_key [Symbol, nil, true]
# @param block [Block] resource block
# @return [String] serialized JSON string
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
def serialize(object = nil, root_key: nil, &block)
resource = resource_with(object, &block)
resource.serialize(root_key: root_key)
end
# Hashify the object with inline definitions
#
# @param object [Object] the object to be serialized
# @param root_key [Symbol, nil, true]
# @param block [Block] resource block
# @return [String] serialized JSON string
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
def hashify(object = nil, root_key: nil, &block)
resource = resource_with(object, &block)
resource.as_json(root_key: root_key)
end
# Enable inference for key and resource name
#
# @param with [Symbol, Class, Module] inflector
# When it's a Symbol, it sets inflector with given name
# When it's a Class or a Module, it sets given object to inflector
# @deprecated Use {.inflector=} instead
def enable_inference!(with:)
Alba::Deprecation.warn('Alba.enable_inference! is deprecated. Use `Alba.inflector=` instead.')
@inflector = inflector_from(with)
@inferring = true
end
# Disable inference for key and resource name
#
# @deprecated Use {.inflector=} instead
def disable_inference!
Alba::Deprecation.warn('Alba.disable_inference! is deprecated. Use `Alba.inflector = nil` instead.')
@inferring = false
@inflector = nil
end
# @deprecated Use {.inflector} instead
# @return [Boolean] whether inference is enabled or not
def inferring
Alba::Deprecation.warn('Alba.inferring is deprecated. Use `Alba.inflector` instead.')
@inferring
end
# Set an inflector
#
# @param inflector [Symbol, Class, Module] inflector
# When it's a Symbol, it accepts `:default`, `:active_support` or `:dry`
# When it's a Class or a Module, it should have some methods, see {Alba::DefaultInflector}
def inflector=(inflector)
@inflector = inflector_from(inflector)
end
# @param block [Block] resource body
# @return [Class<Alba::Resource>] resource class
def resource_class(&block)
klass = Class.new
klass.include(Alba::Resource)
klass.class_eval(&block) if block
klass
end
# @param name [String] a String Alba infers resource name with
# @param nesting [String, nil] namespace Alba tries to find resource class in
# @return [Class<Alba::Resource>] resource class
def infer_resource_class(name, nesting: nil)
raise Alba::Error, 'Inference is disabled so Alba cannot infer resource name. Set inflector before use.' unless Alba.inflector
const_parent = nesting.nil? ? Object : Object.const_get(nesting)
begin
const_parent.const_get("#{inflector.classify(name)}Resource")
rescue NameError # Retry for serializer
const_parent.const_get("#{inflector.classify(name)}Serializer")
end
end
# Configure Alba to symbolize keys
def symbolize_keys!
@symbolize_keys = true
end
# Configure Alba to stringify (not symbolize) keys
def stringify_keys!
@symbolize_keys = false
end
# Regularize key to be either Symbol or String depending on @symbolize_keys
# Returns nil if key is nil
#
# @param key [String, Symbol, nil]
# @return [Symbol, String, nil]
def regularize_key(key)
return if key.nil?
@symbolize_keys ? key.to_sym : key.to_s
end
# Register types, used for both builtin and custom types
#
# @see Alba::Type
# @return [void]
def register_type(name, check: false, converter: nil, auto_convert: false)
@types[name] = Type.new(name, check: check, converter: converter, auto_convert: auto_convert)
end
# Find type by name
#
# @return [Alba::Type]
def find_type(name)
@types.fetch(name) do
raise(Alba::UnsupportedType, "Unknown type: #{name}")
end
end
# Reset config variables
# Useful for test cleanup
def reset!
@encoder = default_encoder
@symbolize_keys = false
@_on_error = :raise
@_on_nil = nil
@types = {}
register_default_types
end
private
# This method could be part of public API, but for now it's private
def resource_with(object, &block)
klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
klass.new(object)
end
def inflector_from(name_or_module)
case name_or_module
when nil then nil
when :default, :active_support
require_relative 'alba/default_inflector'
Alba::DefaultInflector
when :dry
require 'dry/inflector'
Dry::Inflector.new
else validate_inflector(name_or_module)
end
end
def set_encoder_from_backend
@encoder = case @backend
when :oj, :oj_strict then try_oj(mode: :strict)
when :oj_rails then try_oj(mode: :rails)
when :oj_default then try_oj(mode: :default)
when :active_support then try_active_support
when nil, :default, :json then default_encoder
else
raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
end
end
def try_oj(mode:)
require 'oj'
case mode
when :default
->(hash) { Oj.dump(hash) }
else
->(hash) { Oj.dump(hash, mode: mode) }
end
rescue LoadError
Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
default_encoder
end
def try_active_support
require 'active_support/json'
->(hash) { ActiveSupport::JSON.encode(hash) }
rescue LoadError
Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
default_encoder
end
def default_encoder
lambda do |hash|
JSON.generate(hash)
end
end
def validate_inflector(inflector)
unless %i[camelize camelize_lower dasherize classify].all? { |m| inflector.respond_to?(m) }
raise Alba::Error, "Given inflector, #{inflector.inspect} is not valid. It must implement `camelize`, `camelize_lower`, `dasherize` and `classify`."
end
inflector
end
def register_default_types
[String, :String].each do |t|
register_type(t, check: ->(obj) { obj.is_a?(String) }, converter: ->(obj) { obj.to_s })
end
[Integer, :Integer].each do |t|
register_type(t, check: ->(obj) { obj.is_a?(Integer) }, converter: ->(obj) { Integer(obj) })
end
register_type(:Boolean, check: ->(obj) { [true, false].include?(obj) }, converter: ->(obj) { !!obj })
end
end
reset!
end