class Safrano::ServiceBase

Base class for service. Subclass will be for V1, V2 etc…

Constants

DEFAULT_PATH_PREFIX
DEFAULT_SERVER_URL
TRAILING_SLASH
XML_PREAMBLE

Attributes

batch_handler[RW]
cmap[R]

This is just a hash of entity Set Names to the corresponding Class Example Book < Sequel::Model(:book)

@entity_set_name = 'books'

end —> @cmap ends up as {'books' => Book }

collections[RW]

this is just the sorted list of the entity classes (ie… @cmap.values.sorted)

complex_types[RW]
data_service_version[R]

TODO: more elegant design

function_import_keys[RW]
function_imports[RW]
meta[RW]
relman[RW]
type_mappings[RW]
uribase[RW]
v1[RW]

Instance attributes for specialized Version specific Instances

v2[RW]
xname[RW]
xnamespace[RW]
xpath_prefix[RW]
xserver_url[RW]
xtitle[RW]

Public Class Methods

new(&block) click to toggle source
# File lib/safrano/service.rb, line 151
def initialize(&block)
  # Warning: if you add attributes here, you shall need add them
  # in copy_attribs_to as well
  #  because of the version subclasses that dont use "super" initialise
  # (todo: why not??)
  @meta = ServiceMeta.new(self)
  @batch_handler = Safrano::Batch::DisabledHandler.new
  @relman = Safrano::RelationManager.new
  @complex_types = Set.new
  @function_imports = {}
  @function_import_keys = []
  @cmap = {}
  @type_mappings = {}
  instance_eval(&block) if block_given?
end

Public Instance Methods

add_metadata_xml_associations(schema) click to toggle source
# File lib/safrano/service.rb, line 485
def add_metadata_xml_associations(schema)
  @relman.each_rel do |rel|
    rel.with_metadata_info(@xnamespace) do |name, bdinfo|
      assoc = schema.add_element('Association', 'Name' => name)
      bdinfo.each do |bdi|
        assoend = { 'Type' => bdi[:type],
                    'Role' => bdi[:role],
                    'Multiplicity' => bdi[:multiplicity] }
        assoc.add_element('End', assoend)
      end
    end
  end
end
add_metadata_xml_complex_types(schema) click to toggle source
# File lib/safrano/service.rb, line 477
def add_metadata_xml_complex_types(schema)
  @complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema)   }
end
add_metadata_xml_entity_container(schema) click to toggle source
# File lib/safrano/service.rb, line 499
def add_metadata_xml_entity_container(schema)
  ec = schema.add_element('EntityContainer',
                          'Name' => @xname,
                          'm:IsDefaultEntityContainer' => 'true')
  @collections.each do |klass|
    # 3.a Entity set's
    ec.add_element('EntitySet',
                   'Name' => klass.entity_set_name,
                   'EntityType' => klass.type_name)
  end
  # 3.b Association set's
  @relman.each_rel do |rel|
    assoc = ec.add_element('AssociationSet',
                           'Name' => rel.name,
                           'Association' => "#{@xnamespace}.#{rel.name}")

    rel.each_endobj do |eo|
      clazz = Object.const_get(eo)
      assoend = { 'EntitySet' => clazz.entity_set_name.to_s, 'Role' => eo }
      assoc.add_element('End', assoend)
    end
  end

  # 4 function imports
  add_metadata_xml_function_imports(ec)
end
add_metadata_xml_entity_type(schema) click to toggle source
# File lib/safrano/service.rb, line 470
def add_metadata_xml_entity_type(schema)
  @collections.each do |klass|
    enty = klass.add_metadata_rexml(schema)
    klass.add_metadata_navs_rexml(enty, @relman)
  end
end
add_metadata_xml_function_imports(ec) click to toggle source
# File lib/safrano/service.rb, line 481
def add_metadata_xml_function_imports(ec)
  @function_imports.each_value { |func| func.add_metadata_rexml(ec)    }
end
base_url_func_regexp() click to toggle source
# File lib/safrano/service.rb, line 436
def base_url_func_regexp
  @function_import_keys.join('|')
end
base_url_regexp() click to toggle source
Warning: base_url_regexp depends on '@collections', and this needs to be
evaluated after '@collections' is filled !

A regexp matching all allowed base entities (eg product|categories )

# File lib/safrano/service.rb, line 432
def base_url_regexp
  @collections.map(&:entity_set_name).join('|')
end
bugfix_create_response(bool = false) click to toggle source

keep the bug active for now, but allow to activate the fix, later we will change the default to be fixed

# File lib/safrano/service.rb, line 214
def bugfix_create_response(bool = false)
  @bugfix_create_response = bool
end
cmap=(imap) click to toggle source
# File lib/safrano/service.rb, line 342
def cmap=(imap)
  @cmap = imap
  set_collections_sorted(@cmap.values)
end
copy_attribs_to(other) click to toggle source
# File lib/safrano/service.rb, line 232
def copy_attribs_to(other)
  other.cmap = @cmap
  other.collections = @collections
  other.allowed_transitions = @allowed_transitions
  other.xtitle = @xtitle
  other.xname = @xname
  other.xnamespace = @xnamespace
  other.xpath_prefix = @xpath_prefix
  other.xserver_url = @xserver_url
  other.uribase = @uribase
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
  other.relman = @relman
  other.batch_handler = @batch_handler
  other.complex_types = @complex_types
  other.function_imports = @function_imports
  other.function_import_keys = @function_import_keys
  other.type_mappings = @type_mappings
  other
end
enable_batch() click to toggle source
# File lib/safrano/service.rb, line 171
def enable_batch
  @batch_handler = Safrano::Batch::EnabledHandler.new
  (@v1.batch_handler = @batch_handler) if @v1
  (@v2.batch_handler = @batch_handler) if @v2
end
enable_v1_service() click to toggle source
# File lib/safrano/service.rb, line 177
def enable_v1_service
  @v1 = Safrano::ServiceV1.new
  copy_attribs_to @v1
end
enable_v2_service() click to toggle source
# File lib/safrano/service.rb, line 182
def enable_v2_service
  @v2 = Safrano::ServiceV2.new
  copy_attribs_to @v2
end
execute_deferred_iblocks() click to toggle source
# File lib/safrano/service.rb, line 423
def execute_deferred_iblocks
  @collections.each do |k|
    k.instance_eval(&k.deferred_iblock) if k.deferred_iblock
  end
end
finalize_publishing() click to toggle source

to be called at end of publishing block to ensure we get the right names and additionally build the list of valid attribute path's used for validation of $orderby or $filter params

# File lib/safrano/service.rb, line 364
def finalize_publishing
  # build the cmap
  @cmap = {}
  @collections.each do |klass|
    @cmap[klass.entity_set_name] = klass
  end

  # now that we know all model klasses we can handle relationships
  execute_deferred_iblocks

  # set default path prefix if path_prefix was not called
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix

  # set default server url if server_url was not called
  server_url(DEFAULT_SERVER_URL) unless @xserver_url

  set_uribase

  # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
  @collections.each do |klass|
    klass.finalize_publishing(self)

    klass.build_uri(@uribase)

    # Output create (POST) as single entity (Standard) or as array (non-standard buggy)
    klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)

    # define the most optimal casted_values method for the given model(klass)
    if (klass.casted_cols.empty?)
      klass.send(:define_method, :casted_values) do |cols = nil|
        cols ? selected_values_for_odata(cols) : values_for_odata
      end
    else
      klass.send(:define_method, :casted_values) do |cols = nil|
        # we need to dup the model values as we need to change it before passing to_json,
        # but we dont want to interfere with Sequel's owned data
        # (eg because then in worst case it could happen that we write back changed values to DB)
        vals = cols ? selected_values_for_odata(cols) : values_for_odata.dup
        self.class.casted_cols.each { |cc, lambda| vals[cc] = lambda.call(vals[cc]) if vals.key?(cc) }
        vals
      end
    end
  end

  # build allowed transitions (requires that @collections are filled and sorted for having a
  # correct base_url_regexp)
  build_allowed_transitions

  # mixin adapter specific modules where needed
  case Sequel::Model.db.adapter_scheme
  when :postgres
    Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
  when :sqlite
    Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
  else
    Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
  end
end
function_import(name) click to toggle source
# File lib/safrano/service.rb, line 329
def function_import(name)
  funcimp = Safrano::FunctionImport(name)
  @function_imports[name] = funcimp
  @function_import_keys << name
  set_funcimports_sorted
  funcimp
end
metadata_xml(_req) click to toggle source
# File lib/safrano/service.rb, line 526
def metadata_xml(_req)
  doc = REXML::Document.new
  doc.add_element('edmx:Edmx', 'Version' => '1.0')
  doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
  serv = doc.root.add_element('edmx:DataServices',
                              # TODO: export the real version (result from version negotions)
                              # but currently we support only v1 and v2, and most users will use v2
                              'm:DataServiceVersion' => '2.0')
  # 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
  # DataServiceVersion: This attribute MUST be in the data service
  # metadata namespace
  # (http://schemas.microsoft.com/ado/2007/08/dataservices) and SHOULD
  # be present on an
  # edmx:DataServices element [MC-EDMX] to indicate the version of the
  # data service CSDL
  # annotations (attributes in the data service metadata namespace) that
  # are used by the document.
  # Consumers of a data-service metadata endpoint ought to first read this
  # attribute value to determine if
  # they can safely interpret all constructs within the document. The
  # value of this attribute MUST be 1.0
  # unless a "FC_KeepInContent" customizable feed annotation
  # (section 2.2.3.7.2.1) with a value equal to
  # false is present in the CSDL document within the edmx:DataServices
  # node. In this case, the
  # attribute value MUST be 2.0 or greater.
  # In the absence of DataServiceVersion, consumers of the CSDL document
  # can assume the highest DataServiceVersion they can handle.
  serv.add_namespace('xmlns:m', XMLNS::MSFT_ADO_2007_META)

  schema = serv.add_element('Schema',
                            'Namespace' => @xnamespace,
                            'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
  # 1. a. all EntityType
  add_metadata_xml_entity_type(schema)

  # 1. b. all ComplexType
  add_metadata_xml_complex_types(schema)

  # 2. Associations
  add_metadata_xml_associations(schema)

  # 3. Enty container
  add_metadata_xml_entity_container(schema)

  XML_PREAMBLE + doc.to_pretty_xml
end
name(nam) click to toggle source

public API

# File lib/safrano/service.rb, line 188
def name(nam)
  @xname = nam
end
namespace(namsp) click to toggle source
# File lib/safrano/service.rb, line 192
def namespace(namsp)
  @xnamespace = namsp
end
odata_get(req) click to toggle source
# File lib/safrano/service.rb, line 636
def odata_get(req)
  if req.accept?(APPXML)
    # OData V2 reference service implementations are returning app-xml-u8
    # so we do
    [200, CT_APPXML, [service_xml(req)]]
  else
    # this is returned by http://services.odata.org/V2/OData/Safrano.svc
    415
  end
end
path_prefix(path_pr) click to toggle source
# File lib/safrano/service.rb, line 200
def path_prefix(path_pr)
  @xpath_prefix = path_pr.sub(TRAILING_SLASH, '')
  (@v1.xpath_prefix = @xpath_prefix) if @v1
  (@v2.xpath_prefix = @xpath_prefix) if @v2
end
publish_complex_type(ctklass) click to toggle source
# File lib/safrano/service.rb, line 318
def publish_complex_type(ctklass)
  # check that the provided klass is a Safrano ComplexType

  raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType

  serv_namespace = @xnamespace
  ctklass.instance_eval { @namespace = serv_namespace }

  @complex_types.add ctklass
end
publish_media_model(modelklass, entity_set_name = nil, &block) click to toggle source
# File lib/safrano/service.rb, line 310
def publish_media_model(modelklass, entity_set_name = nil, &block)
  register_model(modelklass, entity_set_name, true)
  # we need to execute the passed block in a deferred step
  # after all models have been registered (due to rel. dependancies)
  #      modelklass.instance_eval(&block) if block_given?
  modelklass.deferred_iblock = block if block_given?
end
publish_model(modelklass, entity_set_name = nil, &block) click to toggle source
# File lib/safrano/service.rb, line 302
def publish_model(modelklass, entity_set_name = nil, &block)
  register_model(modelklass, entity_set_name)
  # we need to execute the passed block in a deferred step
  # after all models have been registered (due to rel. dependancies)
  #      modelklass.instance_eval(&block) if block_given?
  modelklass.deferred_iblock = block if block_given?
end
register_model(modelklass, entity_set_name = nil, is_media = false) click to toggle source

this is a central place. We extend Sequel models with OData functionality The included/extended modules depends on the properties(eg, pks, field types) of the model we differentiate

* Single/Multi PK
* Media/Non-Media entity

Putting this logic here in modules loaded once on start shall result in less runtime overhead

# File lib/safrano/service.rb, line 258
def register_model(modelklass, entity_set_name = nil, is_media = false)
  # check that the provided klass is a Sequel Model

  raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods

  if modelklass.ancestors.include? Safrano::Entity
    # modules were already added previously;
    # cleanup state to avoid having data from previous calls
    # mostly usefull for testing (eg API)
    modelklass.reset
  elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
    modelklass.extend Safrano::EntityClassMultiPK
    modelklass.include Safrano::EntityMultiPK
  else
    modelklass.extend Safrano::EntityClassSinglePK
    modelklass.include Safrano::EntitySinglePK
  end

  # Media/Non-media
  if is_media
    modelklass.extend Safrano::EntityClassMedia
    # set default media handler . Can be overridden later with the
    #  "use HandlerKlass, options" API

    modelklass.set_default_media_handler
    modelklass.api_check_media_fields
    modelklass.include Safrano::MediaEntity
  else
    modelklass.extend Safrano::EntityClassNonMedia
    modelklass.include Safrano::NonMediaEntity
  end

  modelklass.prepare_pk
  modelklass.prepare_fields
  esname = (entity_set_name || modelklass).to_s.freeze
  serv_namespace = @xnamespace
  modelklass.instance_eval do
    @entity_set_name = esname
    @namespace = serv_namespace
  end
  @cmap[esname] = modelklass
  set_collections_sorted(@cmap.values)
end
server_url(surl) click to toggle source
# File lib/safrano/service.rb, line 206
def server_url(surl)
  @xserver_url = surl.sub(TRAILING_SLASH, '')
  (@v1.xserver_url = @xserver_url) if @v1
  (@v2.xserver_url = @xserver_url) if @v2
end
service() click to toggle source
# File lib/safrano/service.rb, line 440
def service
  hres = {}
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
  hres
end
service_xml(_req) click to toggle source
# File lib/safrano/service.rb, line 446
def service_xml(_req)
  doc = REXML::Document.new
  # separator required ? ?
  root = doc.add_element('service', 'xml:base' => @uribase)

  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)

  # this generates the main xmlns attribute
  root.add_namespace(XMLNS::W3_2007_APP)
  wp = root.add_element 'workspace'

  title = wp.add_element('atom:title')
  title.text = @xtitle

  @collections.each do |klass|
    col = wp.add_element('collection', 'href' => klass.entity_set_name)
    ct = col.add_element('atom:title')
    ct.text = klass.entity_set_name
  end

  XML_PREAMBLE + doc.to_pretty_xml
end
set_collections_sorted(coll_data) click to toggle source

take care of sorting required to match longest first while parsing with base_url_regexp example: CrewMember must be matched before Crew otherwise we get error

# File lib/safrano/service.rb, line 350
def set_collections_sorted(coll_data)
  @collections = coll_data
  @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
  @collections
end
set_funcimports_sorted() click to toggle source

need to be sorted by size too

# File lib/safrano/service.rb, line 357
def set_funcimports_sorted
  @function_import_keys.sort_by! { |k| k.size }.reverse!
end
set_uribase() click to toggle source

end public API

# File lib/safrano/service.rb, line 220
def set_uribase
  @uribase = if @xpath_prefix.empty?
               @xserver_url
             elsif @xpath_prefix[0] == '/'
               "#{@xserver_url}#{@xpath_prefix}"
             else
               "#{@xserver_url}/#{@xpath_prefix}"
             end
  (@v1.uribase = @uribase) if @v1
  (@v2.uribase = @uribase) if @v2
end
title(tit) click to toggle source
# File lib/safrano/service.rb, line 196
def title(tit)
  @xtitle = tit
end
with_db_type(*dbtypnams, &proc) click to toggle source
# File lib/safrano/service.rb, line 337
def with_db_type(*dbtypnams, &proc)
  m = TypeMapping.builder(*dbtypnams, &proc)
  @type_mappings[m.db_types_rgx] = m
end