module Safrano

needed for ruby < 2.5

picked from activsupport; needed for ruby < 2.5 Destructively converts all keys using the block operations. Same as transform_keys but modifies self.

require 'bigdecimal/util'

filter base class and subclass in our OData namespace

all ordering related classes in our OData module

Error handling

all dataset expanding related classes in our OData module ie do eager loading

top level Safrano namespace

OData relation related classes/module

all dataset selecting related classes in our OData module ie do eager loading

our main namespace

url parameters processing . Mostly delegates to specialised classes (filter, order…) to convert into Sequel exprs.

our main namespace

shamelessly copied from Sequel

monkey patch deactivate Rack/multipart because it does not work on simple OData $batch requests when the content-length is not passed

Link to Model

Note: Safrano has hardcoded mapping rules for most of types. But

it might not be 100% complete
it might not always do what is expected

The type mapping functionality here allows Safrano users to design type mapping themselves and fill or fix the two above issues

Constants

APPATOMXML_UTF8
APPJSON
APPJSON_UTF8
APPXML
APPXML_UTF8
ARY_204_EMPTY_HASH_ARY
COMMA
CONTENT_TYPE

some prominent constants… probably already defined elsewhere eg in Rack but lets KISS

CTT_TYPE_LC
CT_APPXML
CT_ATOMXML
CT_JSON
CT_TEXT
CV_MAX_DATASERVICE_VERSION
CV_MIN_DATASERVICE_VERSION
DB_TYPE_FLOATP_RGX

thank you rubular Test String: DECIMAL (55,2 ) Match groups 1 DECIMAL 2 (55,2 ) 3 55,2 4 55 5 ,2 6 2

DB_TYPE_INTLIKE_RGX

Note: “char” (quoted!) is postgresql's byte type

DB_TYPE_NUMDEC_RGX
DB_TYPE_STRING_RGX

TODO: use Sequel GENERIC_TYPES: –> Constants GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime Time File TrueClass FalseClass'.freeze Classes specifying generic types that Sequel will convert to database-specific types.

EMPTY_ARRAY

frozen empty Array/Hash to reduce unncecessary object creation

EMPTY_HASH
EMPTY_HASH_IN_ARY
EMPTY_STRING
MAX_DATASERVICE_VERSION
MIN_DATASERVICE_VERSION
MP_MIXED
SPACE
TEXTPLAIN_UTF8
TransitionBatch
TransitionContentId
TransitionCount
TransitionEnd
TransitionExecuteFunc
TransitionMetadata
TransitionValue
VERSION

Attributes

allowed_transitions[RW]

Public Class Methods

ComplexType(**props) click to toggle source
Calls superclass method
# File lib/odata/complex_type.rb, line 275
def Safrano.ComplexType(**props)
  Class.new(Safrano::ComplexType) do
    @props = props
    props.each { |a, klassmod|
      asym = a.to_sym
      define_method(asym) do @values[asym] end
      define_method("#{a}=") do |val| @values[asym] = val end
    }
    define_method :initialize do |*p, **kwvals|
      super()
      p.zip(props.keys).each { |val, a| @values[a] = val } if p
      kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
    end
  end
end
FunctionImport(name) click to toggle source
# File lib/odata/function_import.rb, line 8
def self.FunctionImport(name)
  FunctionImport::Function.new(name)
end
add_edm_types(metadata, props) click to toggle source

type mappings are hard, especially between “Standards” like SQL and OData V2 (might be a bit better in V4 ?) this is all best effort/try to make it work logic

# File lib/odata/edm/primitive_types.rb, line 40
def self.add_edm_types(metadata, props)
  # try num/dec with db_type:
  metadata[:edm_type] = if (md = DB_TYPE_NUMDEC_RGX.match(props[:db_type]))
                          prec = md[4]
                          scale = md[6]
                          if (scale && prec)
                            if (scale == '0') # dont force default scale to 0 like SQL standard
                              metadata[:edm_precision] = prec
                              "Edm.Decimal(#{prec})"
                            else
                              # we have precision and scale
                              metadata[:edm_scale] = scale
                              metadata[:edm_precision] = prec
                              "Edm.Decimal(#{prec},#{scale})"
                            end
                          elsif prec
                            # we have precision only
                            metadata[:edm_precision] = prec
                            "Edm.Decimal(#{prec})"
                          else
                            'Edm.Decimal'
                          end
                        end
  return if metadata[:edm_type]

  # try float(prec) with db_type:
  metadata[:edm_type] = if (md = DB_TYPE_FLOATP_RGX.match(props[:db_type]))
                          # FLOAT( 22) match groups
                          # 1 FLOAT
                          # 2 (22 )
                          # 3 22

                          if (prec = md[3])
                            # we have precision only
                            metadata[:edm_precision] = prec
                            'Edm.Double'
                          end
                        end
  return if metadata[:edm_type]

  # try int-like with db_type:
  # smallint|int|integer|bigint|serial|bigserial
  metadata[:edm_type] = if (md = DB_TYPE_INTLIKE_RGX.match(props[:db_type]))

                          if (itype = md[1])
                            case itype.downcase
                            when 'smallint', 'int2', 'smallserial'
                              'Edm.Int16'
                            when 'int', 'integer', 'serial', 'mediumint', 'int4'
                              'Edm.Int32'
                            when 'bigint', 'bigserial', 'int8'
                              'Edm.Int64'
                            when 'tinyint'
                              'Edm.Byte'
                            end
                          end
                        end
  return if metadata[:edm_type]

  # try with Sequel(ruby) type
  metadata[:edm_type] = case props[:type]
                        when :integer
                          'Edm.Int32'
                        when :string
                          'Edm.String'
                        when :date
                          'Edm.DateTime'
                        when :datetime
                          'Edm.DateTime'
                        when :time
                          'Edm.Time'
                        when :boolean
                          'Edm.Boolean'
                        when :float
                          'Edm.Double'
                        when :decimal
                          'Edm.Decimal'
                        when :blob
                          'Edm.Binary'
                        else
                        end
end
create_nav_relation(child, assoc, parent) click to toggle source

link newly created entities(child) to an existing parent by following the association_reflection rules

# File lib/odata/navigation_attribute.rb, line 28
def self.create_nav_relation(child, assoc, parent)
  return unless assoc

  # Note: this coding shares some bits from our sequel/plugins/join_by_paths,
  # method build_unique_join_segments
  # eventually there is an opportunity to have more reusable code here
  case assoc[:type]
  when :one_to_many, :one_to_one
    # sets the FK values in child to corresponding related parent key-values
    # thus creating the "link" between the new entity and the parent
    # if a FK value is already set (not nil/NULL) then only check the
    # consistency with the corresponding parent key-value
    # If the FK value and the parent key value are different, then it's  a
    # a Bad Request error

    leftm = assoc[:model] # should be same as parent.class
    lks = [leftm.primary_key].flatten
    rks = [assoc[:key]].flatten
    join_cond = rks.zip(lks).to_h
    join_cond.each do |rk, lk|
      if child.values[rk] # FK in new entity from payload not nil, only check consistency
        # with the parent - id(s)
        # if (child.values[rk] != parent.pk_hash[lk]) # error...
        # TODO
        # end
      else # we can set the FK value, thus creating the "link"
        child.set(rk => parent.pk_hash[lk])
      end
    end
  when :many_to_one
    # sets the FK values in parent to corresponding related child key-values
    # thus creating the "link" between the new entity and the parent
    # Per design, this can only be called when the FK value is nil
    # from NilNavigationAttribute.odata_post
    lks = [assoc[:key]].flatten
    rks = [child.class.primary_key].flatten
    join_cond = rks.zip(lks).to_h
    join_cond.each do |rk, lk|
      if parent.values[lk] # FK in parent not nil, only check consistency
        # with the child - id(s)
        # if parent.values[lk] != child.pk_hash[rk] # error...
        # TODO
        # end
      else # we can set the FK value, thus creating the "link"
        parent.set(lk => child.pk_hash[rk])
      end
    end
  end
end
remove_nav_relation(assoc, parent) click to toggle source

remove the relation between entity and parent by clearing the FK field(s) (if allowed)

# File lib/odata/navigation_attribute.rb, line 10
def self.remove_nav_relation(assoc, parent)
  return unless assoc

  return unless assoc[:type] == :many_to_one

  # removes/clear the FK values in parent
  # thus deleting the "link" between the entity and the parent
  # Note: This is called if we have to delete the child--> can only be
  # done after removing the FK in parent (if allowed!)
  lks = [assoc[:key]].flatten
  lks.each do |lk|
    parent.set(lk => nil)
    parent.save(transaction: false)
  end
end