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
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)
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
Public Instance Methods
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
# File lib/odata/model_ext.rb, line 112 def attrib_path_valid?(path) @attribute_path_list.include? path end
# 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
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
# File lib/odata/model_ext.rb, line 136 def build_attribute_path_list @attribute_path_list = attribute_path_list end
# 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
# 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
# File lib/odata/model_ext.rb, line 124 def build_expand_path_list @expand_path_list = expand_path_list end
# 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
# File lib/odata/model_ext.rb, line 87 def build_uri(uribase) @uri = "#{uribase}/#{entity_set_name}" end
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
# File lib/odata/model_ext.rb, line 61 def default_entity_set_name @default_entity_set_name end
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
# File lib/odata/model_ext.rb, line 99 def execute_deferred_iblock instance_eval { @deferred_iblock.call } if @deferred_iblock end
# 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
# File lib/odata/model_ext.rb, line 116 def expand_path_valid?(path) @expand_path_list.include? path end
# 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
# File lib/odata/model_ext.rb, line 120 def find_invalid_props(propsset) (propsset - @all_props) unless propsset.subset?(@all_props) end
# File lib/odata/model_ext.rb, line 506 def invalid_hash_data?(data) data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) } end
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
# 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
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
# 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
# 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
# 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
# File lib/odata/model_ext.rb, line 91 def return_as_collection_descriptor Safrano::FunctionImport::ResultDefinition.asEntityColl(self) end
# File lib/odata/model_ext.rb, line 95 def return_as_instance_descriptor Safrano::FunctionImport::ResultDefinition.asEntity(self) end
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
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