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

Refacto activemodel attribute #42

Merged
merged 26 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.sw?
.DS_Store
.rbenv-vars
coverage
rdoc
html
Expand Down
3 changes: 3 additions & 0 deletions couchbase-orm.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]

gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0'
gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0'

gem.add_runtime_dependency 'couchbase'
gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base

gem.add_development_dependency 'rake', '~> 12.2'
gem.add_development_dependency 'rspec', '~> 3.7'
gem.add_development_dependency 'yard', '~> 0.9'
gem.add_development_dependency 'pry'
gem.add_development_dependency 'pry-stack_explorer'
gem.add_development_dependency 'simplecov'

gem.files = `git ls-files`.split("\n")
Expand Down
7 changes: 6 additions & 1 deletion lib/couchbase-orm.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# frozen_string_literal: true, encoding: ASCII-8BIT

require "active_support/lazy_load_hooks"
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path("couchbase-orm/locale/en.yml", __dir__)
end

module CouchbaseOrm
autoload :Error, 'couchbase-orm/error'
autoload :Connection, 'couchbase-orm/connection'
Expand All @@ -8,7 +13,7 @@ module CouchbaseOrm
autoload :HasMany, 'couchbase-orm/utilities/has_many'

def self.logger
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] }
end

def self.logger=(logger)
Expand Down
11 changes: 6 additions & 5 deletions lib/couchbase-orm/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ def has_and_belongs_to_many(name, **options)
old, new = previous_changes[ref]
adds = (new || []) - (old || [])
subs = (old || []) - (new || [])

update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options)
update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options)
update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options) if adds.any?
update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options) if subs.any?
end

after_create save_method
Expand Down Expand Up @@ -167,9 +166,11 @@ def update_has_and_belongs_to_many_reverse_association(assoc, keys, is_add, **op
elsif !is_add && index
tab = tab.dup
tab.delete_at(index)
else
next
end
v.__send__(:"#{remote_method}=", tab)
v.__send__(:save!)
v[remote_method] = tab
v.save!
end
end

Expand Down
163 changes: 58 additions & 105 deletions lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@


require 'active_model'
require 'active_record'
require 'active_record/database_configurations'

require 'active_support/hash_with_indifferent_access'
require 'couchbase'
require 'couchbase-orm/error'
require 'couchbase-orm/views'
require 'couchbase-orm/n1ql'
require 'couchbase-orm/persistence'
require 'couchbase-orm/associations'
require 'couchbase-orm/types'
require 'couchbase-orm/proxies/bucket_proxy'
require 'couchbase-orm/proxies/collection_proxy'
require 'couchbase-orm/utilities/join'
Expand All @@ -19,17 +23,54 @@


module CouchbaseOrm

module ActiveRecordCompat
# try to avoid dependencies on too many active record classes
# by exemple we don't want to go down to the concept of tables

extend ActiveSupport::Concern

module ClassMethods
def primary_key
"id"
end

def base_class?
true
end

def column_names # can't be an alias for now
attribute_names
end
end

def _has_attribute?(attr_name)
attribute_names.include?(attr_name.to_s)
end

def attribute_for_inspect(attr_name)
value = send(attr_name)
value.inspect
end
end

class Base
include ::ActiveModel::Model
include ::ActiveModel::Dirty
include ::ActiveModel::Attributes
include ::ActiveModel::Serializers::JSON

include ::ActiveModel::Validations
include ::ActiveModel::Validations::Callbacks

include ::ActiveRecord::Core
include ActiveRecordCompat

define_model_callbacks :initialize, :only => :after
define_model_callbacks :create, :destroy, :save, :update

include Persistence
include ::ActiveRecord::Timestamp # must be included after Persistence
include Associations
include Views
include N1ql
Expand Down Expand Up @@ -73,33 +114,6 @@ def uuid_generator=(generator)
@uuid_generator = generator
end

def attribute(*names, **options)
@attributes ||= {}
names.each do |name|
name = name.to_sym

@attributes[name] = options

unless self.instance_methods.include?(name)
define_method(name) do
read_attribute(name)
end
end

eq_meth = :"#{name}="
unless self.instance_methods.include?(eq_meth)
define_method(eq_meth) do |value|
value = yield(value) if block_given?
write_attribute(name, value)
end
end
end
end

def attributes
@attributes ||= {}
end

def find(*ids, quiet: false)
CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" }

Expand Down Expand Up @@ -134,53 +148,37 @@ class MismatchTypeError < RuntimeError; end

# Add support for libcouchbase response objects
def initialize(model = nil, ignore_doc_type: false, **attributes)
CouchbaseOrm.logger.debug "Initialize model #{model} with #{attributes}"
simkim marked this conversation as resolved.
Show resolved Hide resolved
@__metadata__ = Metadata.new

# Assign default values
@__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new({type: self.class.design_document})
self.class.attributes.each do |key, options|
default = options[:default]
if default.respond_to?(:call)
write_attribute key, default.call
else
write_attribute key, default
end
end
super()

if model
case model
when Couchbase::Collection::GetResult
CouchbaseOrm.logger.debug "Initialize with Couchbase::Collection::GetResult"
doc = model.content || raise('empty response provided')
type = doc.delete('type')
doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided')
type = doc.delete(:type)
doc.delete(:id)

if type && !ignore_doc_type && type.to_s != self.class.design_document
raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self)
end

@__metadata__.key = attributes[:id]
self.id = attributes[:id] if attributes[:id].present?
@__metadata__.cas = model.cas

# This ensures that defaults are applied
@__attributes__.merge! doc
assign_attributes(doc)
when CouchbaseOrm::Base
CouchbaseOrm.logger.debug "Initialize with CouchbaseOrm::Base"

clear_changes_information
attributes = model.attributes
attributes.delete(:id)
attributes.delete('type')
super(attributes)
super(model.attributes.except(:id, 'type'))
else
clear_changes_information
super(attributes.merge(Hash(model)))
assign_attributes(**attributes.merge(Hash(model)))
end
else
clear_changes_information
super(attributes)
end

yield self if block_given?

run_callbacks :initialize
Expand All @@ -189,64 +187,24 @@ def initialize(model = nil, ignore_doc_type: false, **attributes)

# Document ID is a special case as it is not stored in the document
def id
@__metadata__.key || @id
@id
end

def id=(value)
raise 'ID cannot be changed' if @__metadata__.cas
raise 'ID cannot be changed' if @__metadata__.cas && value
attribute_will_change!(:id)
@id = value.to_s
end

def read_attribute(attr_name)
@__attributes__[attr_name]
end
alias_method :[], :read_attribute

def write_attribute(attr_name, value)
unless value.nil?
coerce = self.class.attributes[attr_name][:type]
value = Kernel.send(coerce.to_s, value) if coerce
end
attribute_will_change!(attr_name) unless @__attributes__[attr_name] == value
@__attributes__[attr_name] = value
end
alias_method :[]=, :write_attribute

#
# Add support for Serialization:
# http://guides.rubyonrails.org/active_model_basics.html#serialization
#

def attributes
copy = @__attributes__.merge({id: id})
copy.delete(:type)
copy
end

def attributes=(attributes)
attributes.each do |key, value|
setter = :"#{key}="
send(setter, value) if respond_to?(setter)
end
@id = value.to_s.presence
end

ID_LOOKUP = ['id', :id].freeze
def attribute(name)
return self.id if ID_LOOKUP.include?(name)
@__attributes__[name]
def [](key)
send(key)
end
alias_method :read_attribute_for_serialization, :attribute

def attribute=(name, value)
__send__(:"#{name}=", value)
def []=(key, value)
CouchbaseOrm.logger.debug "Set attribute #{key} to #{value}"
send(:"#{key}=", value)
end


#
# Add support for comparisons
#

# Public: Allows for access to ActiveModel functionality.
#
# Returns self.
Expand Down Expand Up @@ -279,12 +237,7 @@ def eql?(other)
#
# Returns a boolean.
def ==(other)
case other
when self.class
hash == other.hash
else
false
end
super || other.instance_of?(self.class) && !id.nil? && other.id == id
end
end
end
5 changes: 5 additions & 0 deletions lib/couchbase-orm/locale/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
en:
couchbase:
errors:
messages:
record_invalid: "Validation failed: %{errors}"
38 changes: 23 additions & 15 deletions lib/couchbase-orm/n1ql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module ClassMethods
def n1ql(name, query_fn: nil, emit_key: [], **options)
emit_key = Array.wrap(emit_key)
emit_key.each do |key|
raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && @attributes[key].nil?
raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
end
options = N1QL_DEFAULTS.merge(options)
method_opts = {}
Expand All @@ -49,9 +49,8 @@ def n1ql(name, query_fn: nil, emit_key: [], **options)

singleton_class.__send__(:define_method, name) do |**opts, &result_modifier|
opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus)
values = convert_values(opts.delete(:key))
values = convert_values(method_opts[:emit_key], opts.delete(:key)) if opts[:key]
current_query = run_query(method_opts[:emit_key], values, query_fn, **opts.except(:include_docs))

if result_modifier
opts[:include_docs] = true
current_query.results &result_modifier
Expand All @@ -73,23 +72,32 @@ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil)
validates(attr, presence: true) if validate
n1ql n1ql_method, emit_key: attr

instance_eval "
def self.#{find_method}(#{attr})
#{n1ql_method}(key: #{attr})
end
"
define_singleton_method find_method do |value|
send n1ql_method, key: value
end
end

private

def convert_values(values)
Array.wrap(values).compact.map do |v|
if v.class == String
"'#{N1ql.sanitize(v)}'"
elsif v.class == Date || v.class == Time
"'#{v.iso8601(3)}'"
def convert_values(keys, values)
raise ArgumentError, "Empty keys but values are present, can't type cast" if keys.empty? && Array.wrap(values).any?
keys.zip(Array.wrap(values)).map do |key, value_before_type_cast|
# cast value to type
value = if value_before_type_cast.is_a?(Array)
value_before_type_cast.map do |v|
attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v))
end
else
attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast))
end

CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}"

# then quote and sanitize
if value.class == String
"'#{N1ql.sanitize(value)}'"
else
N1ql.sanitize(v).to_s
N1ql.sanitize(value).to_s
end
end
end
Expand Down
Loading