How to store a string identifier to a model attribute
Asked Answered
I

2

9

I'm using Virtus to create models that represent Salesforce objects.

I'm trying to create attributes that have friendly names that are used to access the value and method that I can use to retrieve a identifier "String" for that variable.

Object.attribute #=> "BOB"
Object.get_identifier(:attribute_name) #=> "KEY"
# OR something like this
Object.attribute.identifier #=> "KEY"

The friendly name is used as the getter/setter and a identifier that I can store each attribute corresponding to the API name.

Here is an example:

class Case
 include Virtus.model

 attribute :case_number, String, identifier: 'Case_Number__c'

end

c = Case.new(case_number: 'XXX')
c.case_number #=> 'XXX'
c.case_number.identifier #=> 'Case_Number__c'

Or, instead of having a method on the Attribute itself, maybe a secondary method gets created for each identifier set:

c.case_number #=> 'XXX'
c.case_number_identifier #=> 'Case_Number__c'

Could I extend Virtus::Attribute and add this? If so, I'm unsure on how to go about it.

Involucel answered 2/3, 2017 at 0:18 Comment(2)
Jaison, what happened to the bounty on this question? It seems as though it just vanished into thin air? Please help me understand.Arnst
i have no idea! im trying to figure out myself why the bounty never completed. I setup another bounty and i'll hook it up after the 24hr waiting timeInvolucel
A
5

Monkey patching Virtus' Attribute class certainly is an option.
However, reaching into the internals of a library makes you vulnerable to refactorings in the private part of that libraries' source code.

Instead, you could use a helper module that encapsulates this feature. Here is a suggestion how:

require 'virtus'

# Put this helper module somewhere on your load path (e.g. your project's lib directory)
module ApiThing

  def self.included(base)
    base.include Virtus.model
    base.extend ApiThing::ClassMethods
  end

  module ClassMethods
    @@identifiers = {}

    def api_attribute(attr_name, *virtus_args, identifier:, **virtus_options)
      attribute attr_name, *virtus_args, **virtus_options
      @@identifiers[attr_name.to_sym] = identifier
    end

    def identifier_for(attr_name)
      @@identifiers.fetch(attr_name.to_sym){ raise ArgumentError, "unknown API attribute #{attr_name.inspect}" }
    end
  end

  def identifier_for(attr_name)
    self.class.identifier_for(attr_name)
  end

end

# And include it in your API classes
class Balls
  include ApiThing

  api_attribute :color,  String,     identifier: 'SOME__fancy_identifier'
  api_attribute :number, Integer,    identifier: 'SOME__other_identifier'
  api_attribute :size,   BigDecimal, identifier: 'THAT__third_identifier'
end

# The attributes will be registered with Virtus – as usual
puts Balls.attribute_set[:color].type  #=> Axiom::Types::String
puts Balls.attribute_set[:number].type #=> Axiom::Types::Integer
puts Balls.attribute_set[:size].type   #=> Axiom::Types::Decimal

# You can use the handy Virtus constructor that takes a hash – as usual
b = Balls.new(color: 'red', number: 2, size: 42)

# You can access the attribute values – as usual
puts b.color      #=> "red"
puts b.number     #=> 2
puts b.size       #=> 0.42e2
puts b.durability #=> undefined method `durability' [...]

# You can ask the instance about identifiers
puts b.identifier_for :color      #=> "SOME__fancy_identifier"
puts b.identifier_for :durability #=> unknown API attribute :durability (ArgumentError)

# And you can ask the class about identifiers
puts Balls.identifier_for :color  #=> "SOME__fancy_identifier"
puts Balls.identifier_for :durability   #=> unknown API attribute :durability (ArgumentError)

You don't need Virtus in order to implement your API identifiers. A similar helper module could just register attr_accessors instead of Virtus attributes.
However, Virtus has other handy features like the hash constructors and attribute coersion. If you don't mind living without these features or finding replacements, ditching Virtus should not be a problem.

HTH! :)

Arnst answered 9/3, 2017 at 21:36 Comment(0)
R
3

Yeah, you've to extend Virtus::Attribute, I could get it to work with:

module Virtus
  class AttributeSet < Module
    def define_identifier(attribute, method_name, visibility, identifier)
      define_method(method_name) { identifier }
      send(visibility, method_name)
    end
  end

  class Attribute
    def define_accessor_methods(attribute_set)
      attribute_set.define_reader_method(self, name,       options[:reader])
      attribute_set.define_writer_method(self, "#{name}=", options[:writer])
      attribute_set.define_identifier(self, "#{name}_identifier", options[:reader], options[:identifier])
    end
  end
end

This could be refactored but you can c.case_number_identifier

Razor answered 4/3, 2017 at 1:44 Comment(3)
What does send(visibility, method_name) do?Arnst
I copied the code from define_reader_method, to make it consistent. send calls the method, maybe virtus override the send method to transform in public_send because visibility is public in this case.Razor
Thank you for this code! Both of the answer solve this problem.Involucel

© 2022 - 2024 — McMap. All rights reserved.