module Safrano::EntityClassBase

class methods. They Make heavy use of Sequel::Model functionality we will add this to our Model classes with “extend” –> self is the Class

Constants

CREATE_AND_SAVE_ENTY_AND_REL
EMPTYH
KEYPRED_URL_REGEXP
MAX_DEPTH
ONLY_INTEGER_RGX

Attributes

casted_cols[R]
cols_metadata[R]

store cols metata here in the model (sub)-class. Initially we stored this infos (eg. edm_types etc) directly into sequels db_schema hash. But this hash is on the upper Sequel::Model(table) class and is shared by all subclasses. By storing it separately here we are less dependant from Sequel, and have less issues with testing with multiples models class derived from same Sequel::Model(table)

data_fields[R]
default_template[R]
deferred_iblock[RW]

initialising block of code to be executed at end of ServerApp.publish_service after all model classes have been registered (without the associations/relationships) typically the block should contain the publication of the associations

entity_id_url_regexp[R]
namespace[R]
nav_collection_attribs[R]
nav_collection_url_regexp[R]
nav_entity_attribs[R]
nav_entity_url_regexp[R]
odata_upk_parts[R]
uri[R]

Public Instance Methods

add_metadata_navs_rexml(schema_enty, relman) click to toggle source

and their Nav attributes == Sequel Model association

# File lib/odata/model_ext.rb, line 213
def add_metadata_navs_rexml(schema_enty, relman)
  @nav_entity_attribs&.each do |ne, klass|
    nattr = metadata_nav_rexml_attribs(ne,
                                       klass,
                                       relman)
    schema_enty.add_element('NavigationProperty', nattr)
  end

  @nav_collection_attribs&.each do |nc, klass|
    nattr = metadata_nav_rexml_attribs(nc,
                                       klass,
                                       relman)
    schema_enty.add_element('NavigationProperty', nattr)
  end
end
add_metadata_rexml(schema) click to toggle source

add metadata xml to the passed REXML schema object

# File lib/odata/model_ext.rb, line 179
def add_metadata_rexml(schema)
  enty = if @media_handler
           schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true')
         else
           schema.add_element('EntityType', 'Name' => to_s)
         end
  # with their properties
  db_schema.each do |pnam, prop|
    metadata = @cols_metadata[pnam]
    if prop[:primary_key] == true
      enty.add_element('Key').add_element('PropertyRef',
                                          'Name' => pnam.to_s)
    end
    attrs = { 'Name' => pnam.to_s,
              #                  'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
              'Type' => metadata[:edm_type] }
    attrs['Nullable'] = 'false' if prop[:allow_null] == false
    enty.add_element('Property', attrs)
  end
  enty
end
add_nav_prop_collection(assoc_symb, attr_name_str = nil) click to toggle source

this functionally similar to the Sequel Rels (many_to_one etc) We need to base this on the Sequel rels, or extend them

# File lib/odata/model_ext.rb, line 300
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
  @nav_collection_attribs = (@nav_collection_attribs || {})
  @nav_collection_attribs_keys = (@nav_collection_attribs_keys || [])
  # DONE: Error handling. This requires that associations
  # have been properly defined with Sequel before
  assoc = all_association_reflections.find do |a|
    a[:name] == assoc_symb && a[:model] == self
  end

  raise Safrano::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc

  attr_class = assoc[:class_name].constantize
  lattr_name_str = (attr_name_str || assoc_symb.to_s)

  # check duplicate attributes names
  raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym

  if @nav_entity_attribs_keys
    raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_entity_attribs_keys.include? lattr_name_str
  end

  @nav_collection_attribs[lattr_name_str] = attr_class
  @nav_collection_attribs_keys << lattr_name_str
  @nav_collection_url_regexp = @nav_collection_attribs_keys.join('|')
end
add_nav_prop_single(assoc_symb, attr_name_str = nil) click to toggle source
# File lib/odata/model_ext.rb, line 326
def add_nav_prop_single(assoc_symb, attr_name_str = nil)
  @nav_entity_attribs = (@nav_entity_attribs || {})
  @nav_entity_attribs_keys = (@nav_entity_attribs_keys || [])
  # DONE: Error handling. This requires that associations
  # have been properly defined with Sequel before
  assoc = all_association_reflections.find do |a|
    a[:name] == assoc_symb && a[:model] == self
  end

  raise Safrano::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc

  attr_class = assoc[:class_name].constantize
  lattr_name_str = (attr_name_str || assoc_symb.to_s)

  # check duplicate attributes names
  raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym

  if @nav_collection_attribs_keys
    raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_collection_attribs_keys.include? lattr_name_str
  end

  @nav_entity_attribs[lattr_name_str] = attr_class
  @nav_entity_attribs_keys << lattr_name_str
  @nav_entity_url_regexp = @nav_entity_attribs_keys.join('|')
end
attrib_path_valid?(path) click to toggle source
# File lib/odata/model_ext.rb, line 112
def attrib_path_valid?(path)
  @attribute_path_list.include? path
end
attribute_path_list(depth = 0) click to toggle source
# File lib/odata/model_ext.rb, line 141
def attribute_path_list(depth = 0)
  ret = @columns_str.dup
  # break circles
  return ret if depth > MAX_DEPTH

  depth += 1

  @nav_entity_attribs&.each do |a, k|
    ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
  end

  @nav_collection_attribs&.each do |a, k|
    ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
  end
  ret
end
build_all_props_list() click to toggle source

list of table columns + all nav attribs –> all props

# File lib/odata/model_ext.rb, line 129
def build_all_props_list
  @all_props = @columns_str.dup
  (@all_props +=  @nav_entity_attribs_keys.map(&:to_s)) if @nav_entity_attribs
  (@all_props +=  @nav_collection_attribs_keys.map(&:to_s)) if @nav_collection_attribs
  @all_props = @all_props.to_set
end
build_attribute_path_list() click to toggle source
# File lib/odata/model_ext.rb, line 136
def build_attribute_path_list
  @attribute_path_list = attribute_path_list
end
build_casted_cols(service) click to toggle source
# File lib/odata/model_ext.rb, line 361
def build_casted_cols(service)
  # cols needed catsting before final json output
  @casted_cols = {}
  db_schema.each { |col, props|
    # first check if we have user-defined type mapping
    usermap = nil
    dbtyp = props[:db_type]
    metadata = @cols_metadata[col]
    if (service.type_mappings.values.find { |map| usermap = map.match(dbtyp) })

      metadata[:edm_type] = usermap.edm_type

      @casted_cols[col] = usermap.castfunc
      next # this will override our rules below !
    end

    if (metadata[:edm_precision] && (metadata[:edm_type] =~ /\AEdm.Decimal\(/i))
      # we save the precision and/or scale in the lambda (binding!)

      @casted_cols[col] = if metadata[:edm_scale]
                            ->(x) {
                              # not sure if these copies are really needed, but feels better that way
                              # output  decimal with precision and scale
                              x&.toDecimalPrecisionScaleString(metadata[:edm_precision], metadata[:edm_scale])
                            }
                          else
                            ->(x) {
                              # not sure if these copies are really needed, but feels better that way
                              # output  decimal with precision only
                              x&.toDecimalPrecisionString(metadata[:edm_precision])
                            }
                          end

      next
    end
    if metadata[:edm_type] == 'Edm.Decimal'
      @casted_cols[col] = ->(x) { x&.toDecimalString }
      next
    end
    # Odata V2 Spec:
    # Edm.Binary    Base64 encoded value of an EDM.Binary value represented as a JSON string
    # See for example https://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)?$format=json
    if metadata[:edm_type] == 'Edm.Binary'
      @casted_cols[col] = ->(x) { Base64.encode64(x) unless x.nil? } # Base64
      next
    end
    # TODO check this more in details
    # NOTE: here we use :type which is the sequel defined ruby-type
    if props[:type] == :datetime
      @casted_cols[col] = ->(x) { x&.iso8601 }

    end
  }
end
build_default_template() click to toggle source
# File lib/odata/model_ext.rb, line 354
def build_default_template
  @default_template = { all_values: EMPTYH }
  if @nav_entity_attribs || @nav_collection_attribs
    @default_template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
  end
end
build_expand_path_list() click to toggle source
# File lib/odata/model_ext.rb, line 124
def build_expand_path_list
  @expand_path_list = expand_path_list
end
build_type_name() click to toggle source
# File lib/odata/model_ext.rb, line 65
def build_type_name
  @type_name = @namespace.to_s.empty? ? to_s : "#{@namespace}.#{self}"
  @default_entity_set_name = to_s
end
build_uri(uribase) click to toggle source
# File lib/odata/model_ext.rb, line 87
def build_uri(uribase)
  @uri = "#{uribase}/#{entity_set_name}"
end
cast_odata_val(val, pk_cast) click to toggle source

super-minimal type check, but better as nothing

# File lib/odata/model_ext.rb, line 519
def cast_odata_val(val, pk_cast)
  pk_cast ? Contract.valid(pk_cast.call(val)) : Contract.valid(val) # no cast needed, eg for string
rescue StandardError => e
  RubyStandardErrorException.new(e)
end
default_entity_set_name() click to toggle source
# File lib/odata/model_ext.rb, line 61
def default_entity_set_name
  @default_entity_set_name
end
entity_set_name() click to toggle source

default for entity_set_name is @default_entity_set_name

# File lib/odata/model_ext.rb, line 71
def entity_set_name
  @entity_set_name = (@entity_set_name || @default_entity_set_name)
end
execute_deferred_iblock() click to toggle source
# File lib/odata/model_ext.rb, line 99
def execute_deferred_iblock
  instance_eval { @deferred_iblock.call } if @deferred_iblock
end
expand_path_list(depth = 0) click to toggle source
# File lib/odata/model_ext.rb, line 158
def expand_path_list(depth = 0)
  ret = []
  ret.concat(@nav_entity_attribs_keys) if @nav_entity_attribs
  ret.concat(@nav_collection_attribs_keys) if @nav_collection_attribs

  # break circles
  return ret if depth > MAX_DEPTH

  depth += 1

  @nav_entity_attribs&.each do |a, k|
    ret.concat(k.expand_path_list(depth).map { |kc| "#{a}/#{kc}" })
  end

  @nav_collection_attribs&.each do |a, k|
    ret.concat(k.expand_path_list(depth).map { |kc| "#{a}/#{kc}" })
  end
  ret
end
expand_path_valid?(path) click to toggle source
# File lib/odata/model_ext.rb, line 116
def expand_path_valid?(path)
  @expand_path_list.include? path
end
finalize_publishing(service) click to toggle source
# File lib/odata/model_ext.rb, line 416
def finalize_publishing(service)
  build_type_name

  # build default output template structure
  build_default_template

  # add edm_types into metadata store
  @cols_metadata = {}
  db_schema.each do |col, props|
    metadata = @cols_metadata.key?(col) ? @cols_metadata[col] : (@cols_metadata[col] = {})
    Safrano.add_edm_types(metadata, props)
  end

  build_casted_cols(service)
  # unless @casted_cols.empty?
  # require 'pry'
  # binding.pry
  # end
  # and finally build the path lists and allowed tr's
  build_attribute_path_list
  build_expand_path_list
  build_all_props_list

  build_allowed_transitions
  build_entity_allowed_transitions

  # for media
  finalize_media if self.respond_to? :finalize_media
end
find_invalid_props(propsset) click to toggle source
# File lib/odata/model_ext.rb, line 120
def find_invalid_props(propsset)
  (propsset - @all_props) unless propsset.subset?(@all_props)
end
invalid_hash_data?(data) click to toggle source
# File lib/odata/model_ext.rb, line 506
def invalid_hash_data?(data)
  data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
end
metadata_nav_rexml_attribs(assoc, to_klass, relman) click to toggle source

metadata REXML data for a single Nav attribute

# File lib/odata/model_ext.rb, line 202
def metadata_nav_rexml_attribs(assoc, to_klass, relman)
  from = to_s
  to = to_klass.to_s
  relman.get_metadata_xml_attribs(from,
                                  to,
                                  association_reflection(assoc.to_sym)[:type],
                                  @namespace,
                                  assoc)
end
new_from_hson_h(hash) click to toggle source

Factory json-> Model Object instance

# File lib/odata/model_ext.rb, line 104
def new_from_hson_h(hash)
  # enty = new
  # enty.set_fields(hash, data_fields, missing: :skip)
  enty = create(hash)
  # enty.set(hash)
  enty
end
odata_create_save_entity_and_rel(req, new_entity, assoc, parent) click to toggle source
# File lib/odata/model_ext.rb, line 538
def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
  if req.in_changeset
    # in-changeset requests get their own transaction
    CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
  else
    db.transaction do
      CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
    end
  end
end
output_template(expand_list:, select: Safrano::SelectBase::ALL) click to toggle source

Recursive this method is performance critical. Called at least once for every request

# File lib/odata/model_ext.rb, line 231
def output_template(expand_list:,
                    select: Safrano::SelectBase::ALL)

  return @default_template if expand_list.empty? && select.all_props?

  template = {}
  expand_e = {}
  expand_c = {}
  deferr = []

  # 1. handle non-navigation properties, only consider $select
  # 2. handle navigations properties, need to check $select and $expand
  if select.all_props?

    template[:all_values] = EMPTYH

    # include all nav attributes -->
    @nav_entity_attribs&.each do |attr, klass|
      if expand_list.key?(attr)
        expand_e[attr] = klass.output_template(expand_list: expand_list[attr])
      else
        deferr << attr
      end
    end

    @nav_collection_attribs&.each do |attr, klass|
      if expand_list.key?(attr)
        expand_c[attr] = klass.output_template(expand_list: expand_list[attr])
      else
        deferr << attr
      end
    end

  else
    template[:selected_vals] = @columns_str & select.props

    # include only selected nav attribs-->need additional intersection step
    if @nav_entity_attribs
      selected_nav_e = @nav_entity_attribs_keys & select.props

      selected_nav_e&.each do |attr|
        if expand_list.key?(attr)
          klass = @nav_entity_attribs[attr]
          expand_e[attr] = klass.output_template(expand_list: expand_list[attr])
        else
          deferr << attr
        end
      end
    end
    if @nav_collection_attribs
      selected_nav_c = @nav_collection_attribs_keys & select.props
      selected_nav_c&.each do |attr|
        if expand_list.key?(attr)
          klass = @nav_collection_attribs[attr]
          expand_c[attr] = klass.output_template(expand_list: expand_list[attr])
        else
          deferr << attr
        end
      end
    end
  end
  template[:expand_e] = expand_e
  template[:expand_c] = expand_c
  template[:deferr] = deferr
  template
end
prepare_fields() click to toggle source
# File lib/odata/model_ext.rb, line 497
def prepare_fields
  # columns as strings
  @columns_str = @columns.map(&:to_s)

  @data_fields = db_schema.map do |col, cattr|
    cattr[:primary_key] ? nil : col
  end.select { |col| col }
end
prepare_pk() click to toggle source
# File lib/odata/model_ext.rb, line 447
def prepare_pk
  if primary_key.is_a? Array
    @pk_names = []
    @pk_cast_from_string = {}
    odata_upk_build = []
    primary_key.each { |pk|
      @pk_names << pk.to_s
      kvpredicate = case db_schema[pk][:type]
                    when :integer
                      @pk_cast_from_string[pk] = ->(str) { Integer(str) }
                      '?'
                    else
                      "'?'"
                    end
      odata_upk_build << "#{pk}=#{kvpredicate}"
    }
    @odata_upk_parts = odata_upk_build.join(',').split('?')

    # regex parts for unordered matching
    @iuk_rgx_parts = primary_key.map { |pk|
      kvpredicate = case db_schema[pk][:type]
                    when :integer
                      "(\\d+)"
                    else
                      "'(\\w+)'"
                    end
      [pk, "#{pk}=#{kvpredicate}"]
    }.to_h

    # single regex assuming the key fields are ordered !
    @iuk_rgx = /\A#{@iuk_rgx_parts.values.join(',\s*')}\z/

    @iuk_rgx_parts.transform_values! { |v| /\A#{v}\z/ }

    @entity_id_url_regexp = KEYPRED_URL_REGEXP
  else
    @pk_names = [primary_key.to_s]
    @pk_cast_from_string = nil
    kvpredicate = case db_schema[primary_key][:type]
                  when :integer
                    @pk_cast_from_string = ->(str) { Integer(str) }
                    "(\\d+)"
                  else
                    "'(\\w+)'"
                  end
    @iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
    @entity_id_url_regexp = KEYPRED_URL_REGEXP
  end
end
reset() click to toggle source
# File lib/odata/model_ext.rb, line 75
def reset
  # TODO: automatically reset all attributes?
  @deferred_iblock = nil
  @entity_set_name = nil
  @uri = nil
  @odata_upk_parts = nil
  @uparms = nil
  @params = nil
  @cx = nil
  @cols_metadata = {}
end
return_as_collection_descriptor() click to toggle source
# File lib/odata/model_ext.rb, line 91
def return_as_collection_descriptor
  Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
end
return_as_instance_descriptor() click to toggle source
# File lib/odata/model_ext.rb, line 95
def return_as_instance_descriptor
  Safrano::FunctionImport::ResultDefinition.asEntity(self)
end
transition_attribute_regexp() click to toggle source

A regexp matching all allowed attributes of the Entity (eg ID|name|size etc… ) at start position and returning the rest

# File lib/odata/model_ext.rb, line 512
def transition_attribute_regexp
  #      db_schema.map { |sch| sch[0] }.join('|')
  # @columns is from Sequel Model
  %r{\A/(#{@columns.join('|')})(.*)\z}
end
type_name() click to toggle source

convention: entityType is the namepsaced Ruby Model class –> name is just to_s Warning: for handling Navigation relations, we use anonymous collection classes dynamically subtyped from a Model class, and in such an anonymous class the class-name is not the OData Type. In these subclass we redefine “type_name” thus when we need the Odata type name, we shall use this method instead of just the collection class name

# File lib/odata/model_ext.rb, line 57
def type_name
  @type_name
end