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
@return [String] class name as string
# File lib/diaspora_federation/entity.rb, line 163 def self.class_name name.rpartition("::").last end
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
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
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
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
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
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
# File lib/diaspora_federation/entity.rb, line 128 def self.json_parser_class DiasporaFederation::Parsers::JsonParser end
# File lib/diaspora_federation/entity.rb, line 118 def self.xml_parser_class DiasporaFederation::Parsers::XmlParser end
Public Instance Methods
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
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
@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
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
# 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
default: nothing to enrich
# File lib/diaspora_federation/entity.rb, line 293 def enriched_properties normalized_properties end
# 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
# 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
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
# 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
# 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
# File lib/diaspora_federation/entity.rb, line 275 def normalized_properties properties.to_h {|name, value| [name, normalize_property(name, value)] } end
# File lib/diaspora_federation/entity.rb, line 341 def optional_nil_value?(name, value) value.nil? && self.class.optional_props.include?(name) end
@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
# 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
# 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
# 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
# File lib/diaspora_federation/entity.rb, line 217 def setable_property?(type, val) setable_string?(type, val) || (type == :timestamp && val.is_a?(Time)) end
# 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
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
# 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
# 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
default: no special order
# File lib/diaspora_federation/entity.rb, line 298 def xml_elements enriched_properties end