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
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 }
this is just the sorted list of the entity classes (ie… @cmap.values.sorted)
TODO: more elegant design
Instance attributes for specialized Version specific Instances
Public Class Methods
# 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
# 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
# File lib/safrano/service.rb, line 477 def add_metadata_xml_complex_types(schema) @complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) } end
# 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
# 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
# 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
# File lib/safrano/service.rb, line 436 def base_url_func_regexp @function_import_keys.join('|') end
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
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
# File lib/safrano/service.rb, line 342 def cmap=(imap) @cmap = imap set_collections_sorted(@cmap.values) end
# 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
# 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
# File lib/safrano/service.rb, line 177 def enable_v1_service @v1 = Safrano::ServiceV1.new copy_attribs_to @v1 end
# File lib/safrano/service.rb, line 182 def enable_v2_service @v2 = Safrano::ServiceV2.new copy_attribs_to @v2 end
# 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
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
# 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
# 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
public API
# File lib/safrano/service.rb, line 188 def name(nam) @xname = nam end
# File lib/safrano/service.rb, line 192 def namespace(namsp) @xnamespace = namsp end
# 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
# 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
# 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
# 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
# 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
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
# 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
# File lib/safrano/service.rb, line 440 def service hres = {} hres['d'] = { 'EntitySets' => @collections.map(&:type_name) } hres end
# 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
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
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
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
# File lib/safrano/service.rb, line 196 def title(tit) @xtitle = tit end
# 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