Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic attribute #79

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/couchbase-orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module CouchbaseOrm
autoload :Document, 'couchbase-orm/base'
autoload :NestedDocument, 'couchbase-orm/base'
autoload :HasMany, 'couchbase-orm/utilities/has_many'
autoload :AttributesDynamic, 'couchbase-orm/attributes/dynamic'

def self.logger
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] }
Expand Down
99 changes: 99 additions & 0 deletions lib/couchbase-orm/attributes/dynamic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# frozen_string_literal: true, encoding: ASCII-8BIT

module CouchbaseOrm
module AttributesDynamic
extend ActiveSupport::Concern

# Override respond_to? so it responds properly for dynamic attributes.
#
# @example Does this object respond to the method?
# person.respond_to?(:title)
#
# @param [ Array ] name The name of the method.
# @param [ true | false ] include_private
#
# @return [ true | false ] True if it does, false if not.
def respond_to?(name, include_private = false)
super || attributes&.key?(name.to_s.reader)
end

private

# Override private _assign_attribute to accept dynamic attribute
#
# @param [ String ] name of attribute
# @param [ Object ] value of attribute
#
# @return [ Object ] value of attribute
def _assign_attribute(name, value)
responds = name.reader == 'id' || respond_to?(name.writer)
if responds
public_send(name.writer, value)
else
type = value.class.to_s.underscore.to_sym
type = :hash if type == :"active_support/hash_with_indifferent_access"
type = ActiveModel::Type.lookup(type)
@attributes[name] = ActiveModel::Attribute.from_database(name, value, type)
end
end

# Define a reader method for a dynamic attribute.
#
# @example Define a reader method.
# model.define_dynamic_reader(:field)
#
# @param [ String ] name The name of the field.
def define_dynamic_reader(name)
return unless name.valid_method_name?

instance_eval do
define_singleton_method(name) do
@attributes[getter].value
end
end
end

# Define a writer method for a dynamic attribute.
#
# @example Define a writer method.
# model.define_dynamic_writer(:field)
#
# @param [ String ] name The name of the field.
def define_dynamic_writer(name)
return unless name.valid_method_name?

instance_eval do
define_singleton_method("#{name}=") do |value|
@attributes.write_from_user(name, value)
value
end
end
end

# Used for allowing accessor methods for dynamic attributes.
#
# @api private
#
# @example Call through method_missing.
# document.method_missing(:test)
#
# @param [ String | Symbol ] name The name of the method.
# @param [ Object... ] *args The arguments to the method.
#
# @return [ Object ] The result of the method call.
def method_missing(name, *args)
attr = name.to_s
return super unless attr.reader != 'id' && attributes.key?(attr.reader)

getter = attr.reader
if attr.writer?
define_dynamic_writer(getter)
@attributes.write_from_user(getter, args.first)
args.first
else
define_dynamic_reader(getter)
@attributes[getter].value
end
end
end
end
21 changes: 20 additions & 1 deletion lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
end
require 'active_support/hash_with_indifferent_access'
require 'couchbase'
require 'couchbase-orm/extensions/string'
require 'couchbase-orm/error'
require 'couchbase-orm/views'
require 'couchbase-orm/n1ql'
Expand Down Expand Up @@ -86,7 +87,17 @@ def attribute_for_inspect(attr_name)
value.inspect
end

if ActiveModel::VERSION::MAJOR < 6
if ActiveModel::VERSION::MAJOR <= 6
def format_for_inspect(value)
if value.is_a?(String) && value.length > 50
"#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
else
value.inspect
end
end

def attribute_names
self.class.attribute_names
end
Expand All @@ -104,6 +115,14 @@ def _write_attribute(attr_name, value)
@attributes.write_from_user(attr_name.to_s, value)
value
end

def read_attribute(attr_name, &block)
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name

name = @primary_key if name == "id" && @primary_key
@attributes.fetch_value(name, &block)
end
end
end

Expand Down
27 changes: 27 additions & 0 deletions lib/couchbase-orm/extensions/string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module CouchbaseOrm
module Extensions
module String
def reader
delete("=").sub(/\_before\_type\_cast\z/, '')
end

def writer
sub(/\_before\_type\_cast\z/, '') + "="
end

def writer?
include?("=")
end

def before_type_cast?
ends_with?("_before_type_cast")
end

def valid_method_name?
/[@$"-]/ !~ self
end
end
end
end

::String.__send__(:include, CouchbaseOrm::Extensions::String)
2 changes: 1 addition & 1 deletion lib/couchbase-orm/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require "couchbase-orm/types/nested"
require "couchbase-orm/types/encrypted"

if ActiveModel::VERSION::MAJOR < 6
if ActiveModel::VERSION::MAJOR <= 6
# In Rails 5, the type system cannot allow overriding the default types
ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k|
k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp)
Expand Down
27 changes: 27 additions & 0 deletions spec/attribute_dynamic_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true, encoding: ASCII-8BIT

require File.expand_path("../support", __FILE__)

class AttributeDynamicTest < CouchbaseOrm::Base
include CouchbaseOrm::AttributesDynamic

attribute :name, :string
attribute :job, :string
end

describe CouchbaseOrm::AttributesDynamic do
context 'from initialize' do
it 'should accept unknown attribute from initialize' do
dynamic = AttributeDynamicTest.new(name: 'joe', new_attribute: 1)
expect(dynamic.new_attribute).to eq(1)
end
end

context 'from Couchbase' do
it 'should accept unknown attribute from Couchbase' do
dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2)
expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2)
dynamic.destroy
end
end
end
Loading