class DiasporaFederation::Entity

Entity is the base class for all other objects used to encapsulate data for federation messages in the diaspora* network. Entity fields are specified using a simple {PropertiesDSL DSL} as part of the class definition.

Any entity also provides the means to serialize itself and all nested entities to XML (for deserialization from XML to Entity instances, see {Salmon::XmlPayload}).

@abstract Subclass and specify properties to implement various entities.

@example Entity subclass definition

class MyEntity < Entity
  property :prop
  property :optional, default: false
  property :dynamic_default, default: -> { Time.now }
  entity :nested, NestedEntity
  entity :multiple, [OtherEntity]
end

@example Entity instantiation

nentity = NestedEntity.new
oe1 = OtherEntity.new
oe2 = OtherEntity.new

entity = MyEntity.new(prop: 'some property',
                      nested: nentity,
                      multiple: [oe1, oe2])

@note Entity properties can only be set during initialization, after that the

entity instance becomes frozen and must not be modified anymore. Instances
are intended to be immutable data containers, only.

Constants

ENTITY_NAME_REGEX

Regex to validate and find entity names

INVALID_XML_REGEX

Invalid XML characters @see www.w3.org/TR/REC-xml/#charsets “Extensible Markup Language (XML) 1.0”

Public Class Methods

class_name() click to toggle source

@return [String] class name as string

# File lib/diaspora_federation/entity.rb, line 163
def self.class_name
  name.rpartition("::").last
end
entity_class(entity_name) click to toggle source

Transform the given String from the lowercase underscored version to a camelized variant and returns the Class constant.

@see .entity_name

@param [String] entity_name “snake_case” class name @return [Class] entity class

# File lib/diaspora_federation/entity.rb, line 151
def self.entity_class(entity_name)
  raise InvalidEntityName, "'#{entity_name}' is invalid" unless entity_name =~ /\A#{ENTITY_NAME_REGEX}\z/

  class_name = entity_name.sub(/\A[a-z]/, &:upcase)
  class_name.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase }

  raise UnknownEntity, "'#{class_name}' not found" unless Entities.const_defined?(class_name)

  Entities.const_get(class_name)
end
entity_name() click to toggle source

Makes an underscored, lowercase form of the class name

@see .entity_class

@return [String] entity name

# File lib/diaspora_federation/entity.rb, line 137
def self.entity_name
  class_name.tap do |word|
    word.gsub!(/(.)([A-Z])/, '\1_\2')
    word.downcase!
  end
end
from_hash(properties_hash) click to toggle source

Creates an instance of self, filling it with data from a provided hash of properties.

The hash format is described as following:
1) Properties of the hash are representation of the entity’s class properties
2) Keys of the hash must be of Symbol type
3) Possible values of the hash properties depend on the types of the entity’s class properties
4) Basic properties, such as booleans, strings, integers and timestamps are represented by values of respective formats
5) Nested hashes and arrays of hashes are allowed to represent nested entities. Nested hashes follow the same format as the parent hash.
6) Besides, the nested entities can be passed in the hash as already instantiated objects of the respective type.

@param [Hash] properties_hash A hash of the expected format @return [Entity] an instance

# File lib/diaspora_federation/entity.rb, line 195
def self.from_hash(properties_hash)
  new(properties_hash)
end
from_json(json_hash) click to toggle source

Creates an instance of self by parsing a hash in the format of JSON serialized object (which usually means data from a parsed JSON input).

# File lib/diaspora_federation/entity.rb, line 124
def self.from_json(json_hash)
  from_hash(*json_parser_class.new(self).parse(json_hash))
end
from_xml(root_node) click to toggle source

Construct a new instance of the given Entity and populate the properties with the attributes found in the XML. Works recursively on nested Entities and Arrays thereof.

@param [Nokogiri::XML::Element] root_node xml nodes @return [Entity] instance

# File lib/diaspora_federation/entity.rb, line 114
def self.from_xml(root_node)
  from_hash(*xml_parser_class.new(self).parse(root_node))
end
new(data) click to toggle source

Initializes the Entity with the given attribute hash and freezes the created instance it returns.

After creation, the entity is validated against a Validator, if one is defined. The Validator needs to be in the {DiasporaFederation::Validators} namespace and named like “<EntityName>Validator”. Only valid entities can be created.

@see DiasporaFederation::Validators

@note Attributes not defined as part of the class definition ({PropertiesDSL#property},

{PropertiesDSL#entity}) get discarded silently.

@param [Hash] data entity data @return [Entity] new instance

# File lib/diaspora_federation/entity.rb, line 61
def initialize(data)
  logger.debug "create entity #{self.class} with data: #{data}"
  raise ArgumentError, "expected a Hash" unless data.is_a?(Hash)

  entity_data = self.class.resolv_aliases(data)
  validate_missing_props(entity_data)

  self.class.default_values.merge(entity_data).each do |name, value|
    instance_variable_set("@#{name}", instantiate_nested(name, nilify(value))) if setable?(name, value)
  end

  freeze
  validate
end

Private Class Methods

json_parser_class() click to toggle source
# File lib/diaspora_federation/entity.rb, line 128
                     def self.json_parser_class
  DiasporaFederation::Parsers::JsonParser
end
xml_parser_class() click to toggle source
# File lib/diaspora_federation/entity.rb, line 118
                     def self.xml_parser_class
  DiasporaFederation::Parsers::XmlParser
end

Public Instance Methods

to_h() click to toggle source

Returns a Hash representing this Entity (attributes => values). Nested entities are also converted to a Hash. @return [Hash] entity data (mostly equal to the hash used for initialization).

# File lib/diaspora_federation/entity.rb, line 79
def to_h
  enriched_properties.to_h {|key, value|
    type = self.class.class_props[key]

    if type.instance_of?(Symbol) || value.nil?
      [key, value]
    elsif type.instance_of?(Class)
      [key, value.to_h]
    elsif type.instance_of?(Array)
      [key, value.map(&:to_h)]
    end
  }
end
to_json(*_args) click to toggle source

Renders entity to a hash representation of the entity JSON format @return [Hash] Returns a hash that is equal by structure to the entity in JSON format

# File lib/diaspora_federation/entity.rb, line 174
def to_json(*_args)
  {
    entity_type: self.class.entity_name,
    entity_data: json_data
  }
end
to_s() click to toggle source

@return [String] string representation of this object

# File lib/diaspora_federation/entity.rb, line 168
def to_s
  "#{self.class.class_name}#{":#{guid}" if respond_to?(:guid)}"
end
to_xml() click to toggle source

Returns the XML representation for this entity constructed out of {www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}s

@see Nokogiri::XML::Node.to_xml

@return [Nokogiri::XML::Element] root element containing properties as child elements

# File lib/diaspora_federation/entity.rb, line 99
def to_xml
  doc = Nokogiri::XML::Document.new
  Nokogiri::XML::Element.new(self.class.entity_name, doc).tap do |root_element|
    xml_elements.each do |name, value|
      add_property_to_xml(doc, root_element, name, value)
    end
  end
end

Private Instance Methods

add_property_to_xml(doc, root_element, name, value) click to toggle source
# File lib/diaspora_federation/entity.rb, line 302
def add_property_to_xml(doc, root_element, name, value)
  if [String, TrueClass, FalseClass, Integer].any? {|c| value.is_a? c }
    root_element << simple_node(doc, name, value.to_s)
  else
    # call #to_xml for each item and append to root
    [*value].compact.each do |item|
      child = item.to_xml
      root_element << child if child
    end
  end
end
enriched_properties() click to toggle source

default: nothing to enrich

# File lib/diaspora_federation/entity.rb, line 293
def enriched_properties
  normalized_properties
end
error_message(validator) click to toggle source
# File lib/diaspora_federation/entity.rb, line 261
def error_message(validator)
  errors = validator.errors.map do |prop, rule|
    "property: #{prop}, value: #{public_send(prop).inspect}, rule: #{rule[:rule]}, with params: #{rule[:params]}"
  end
  "Failed validation for #{self}#{" from #{author}" if respond_to?(:author)} for properties: #{errors.join(' | ')}"
end
instantiate_nested(name, value) click to toggle source
# File lib/diaspora_federation/entity.rb, line 240
def instantiate_nested(name, value)
  if value.instance_of?(Array)
    return value unless value.first.instance_of?(Hash)

    value.map {|hash| self.class.class_props[name].first.new(hash) }
  elsif value.instance_of?(Hash)
    self.class.class_props[name].new(value)
  else
    value
  end
end
json_data() click to toggle source

Generates a hash with entity properties which is put to the “entity_data” field of a JSON serialized object. @return [Hash] object properties in JSON format

# File lib/diaspora_federation/entity.rb, line 324
def json_data # rubocop:disable Metrics/PerceivedComplexity
  enriched_properties.map {|key, value|
    type = self.class.class_props[key]
    next if optional_nil_value?(key, value)

    if !value.nil? && type.instance_of?(Class)
      entity_data = value.to_json
      [key, entity_data] unless entity_data.nil?
    elsif type.instance_of?(Array)
      entity_data = value&.map(&:to_json)
      [key, entity_data] unless entity_data.nil?
    else
      [key, value]
    end
  }.compact.to_h
end
nilify(value) click to toggle source
# File lib/diaspora_federation/entity.rb, line 234
def nilify(value)
  return nil if value.respond_to?(:empty?) && value.empty? && !value.instance_of?(Array)

  value
end
normalize_property(name, value) click to toggle source
# File lib/diaspora_federation/entity.rb, line 279
def normalize_property(name, value)
  return nil if optional_nil_value?(name, value)

  case self.class.class_props[name]
  when :string
    value.to_s.gsub(INVALID_XML_REGEX, "\uFFFD")
  when :timestamp
    value.nil? ? "" : value.utc.iso8601
  else
    value
  end
end
normalized_properties() click to toggle source
# File lib/diaspora_federation/entity.rb, line 275
def normalized_properties
  properties.to_h {|name, value| [name, normalize_property(name, value)] }
end
optional_nil_value?(name, value) click to toggle source
# File lib/diaspora_federation/entity.rb, line 341
def optional_nil_value?(name, value)
  value.nil? && self.class.optional_props.include?(name)
end
properties() click to toggle source

@return [Hash] hash with all properties

# File lib/diaspora_federation/entity.rb, line 269
def properties
  self.class.class_props.keys.each_with_object({}) do |prop, hash|
    hash[prop] = public_send(prop)
  end
end
setable?(name, val) click to toggle source
# File lib/diaspora_federation/entity.rb, line 210
def setable?(name, val)
  type = self.class.class_props[name]
  return false if type.nil? # property undefined

  setable_property?(type, val) || setable_nested?(type, val) || setable_multi?(type, val)
end
setable_multi?(type, val) click to toggle source
# File lib/diaspora_federation/entity.rb, line 229
def setable_multi?(type, val)
  type.instance_of?(Array) && val.instance_of?(Array) &&
    (val.all? {|v| v.instance_of?(type.first) } || val.all? {|v| v.instance_of?(Hash) })
end
setable_nested?(type, val) click to toggle source
# File lib/diaspora_federation/entity.rb, line 225
def setable_nested?(type, val)
  type.instance_of?(Class) && type.ancestors.include?(Entity) && (val.is_a?(Entity) || val.is_a?(Hash))
end
setable_property?(type, val) click to toggle source
# File lib/diaspora_federation/entity.rb, line 217
def setable_property?(type, val)
  setable_string?(type, val) || (type == :timestamp && val.is_a?(Time))
end
setable_string?(type, val) click to toggle source
# File lib/diaspora_federation/entity.rb, line 221
def setable_string?(type, val)
  %i[string integer boolean].include?(type) && val.respond_to?(:to_s)
end
simple_node(doc, name, value) click to toggle source

Create simple node, fill it with text and append to root

# File lib/diaspora_federation/entity.rb, line 315
def simple_node(doc, name, value)
  Nokogiri::XML::Element.new(name.to_s, doc).tap do |node|
    node.content = value unless value.empty?
  end
end
validate() click to toggle source
# File lib/diaspora_federation/entity.rb, line 252
def validate
  validator_name = "#{self.class.name.split('::').last}Validator"
  return unless Validators.const_defined? validator_name

  validator_class = Validators.const_get validator_name
  validator = validator_class.new self
  raise ValidationError, error_message(validator) unless validator.valid?
end
validate_missing_props(entity_data) click to toggle source
# File lib/diaspora_federation/entity.rb, line 201
def validate_missing_props(entity_data)
  missing_props = self.class.missing_props(entity_data)
  return if missing_props.empty?

  obj_str = "#{self.class.class_name}#{":#{entity_data[:guid]}" if entity_data.has_key?(:guid)}" \
            "#{" from #{entity_data[:author]}" if entity_data.has_key?(:author)}"
  raise ValidationError, "#{obj_str}: Missing required properties: #{missing_props.join(', ')}"
end
xml_elements() click to toggle source

default: no special order

# File lib/diaspora_federation/entity.rb, line 298
def xml_elements
  enriched_properties
end