module FHIR class ResourceAddress DEFAULTS = { id: nil, resource: nil, format: 'application/fhir+xml' }.freeze DEFAULT_CHARSET = 'utf-8'.freeze DEFAULT_CONTENT_TYPE = 'application/fhir+xml'.freeze # time to default to json? # # Normalize submitted header key value pairs # # 'content-type', 'Content-Type', and :content_type would all represent the content-type header # # Assumes symbols like :content_type to be "content-type" # if for some odd reason the Header string representation contains underscores it would need to be specified # as a string (i.e. options {"Underscore_Header" => 'why not hyphens'}) # Note that servers like apache or nginx consider that invalid anyways and drop them # http://httpd.apache.org/docs/trunk/new_features_2_4.html # http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers def self.normalize_headers(to_be_normalized, to_symbol = true, capitalized = false) to_be_normalized.inject({}) do |result, (key, value)| key = key.to_s.downcase.split(/-|_/) key.map!(&:capitalize) if capitalized key = to_symbol ? key.join('_').to_sym : key.join('-') result[key] = value.to_s result end end def self.convert_symbol_headers headers headers.inject({}) do |result, (key, value)| if key.is_a? Symbol key = key.to_s.split(/_/).map(&:capitalize).join('-') end result[key] = value.to_s result end end # Returns normalized HTTP Headers # header key value pairs can be supplied with keys specified as symbols or strings # keys will be normalized to symbols. # e.g. the keys :accept, "accept", and "Accept" all represent the Accept HTTP Header # @param [Hash] options key value pairs for the http headerx # @return [Hash] The normalized FHIR Headers def self.fhir_headers(headers = {}, additional_headers = {}, format = DEFAULT_CONTENT_TYPE, use_accept_header = true, use_accept_charset = true) # normalizes header names to be case-insensitive # See relevant HTTP RFCs: # https://tools.ietf.org/html/rfc2616#section-4.2 # https://tools.ietf.org/html/rfc7230#section-3.2 # # https://tools.ietf.org/html/rfc7231#section-5.3.2 # optional white space before and # https://tools.ietf.org/html/rfc2616#section-3.4 # utf-8 is case insensitive # headers ||= {} additional_headers ||= {} fhir_headers = {user_agent: 'Ruby FHIR Client'} fhir_headers[:accept_charset] = DEFAULT_CHARSET if use_accept_charset # https://www.hl7.org/fhir/DSTU2/http.html#mime-type # could add option for ;charset=#{DEFAULT_CHARSET} in accept header fhir_headers[:accept] = "#{format}" if use_accept_header # maybe in a future update normalize everything to symbols # Headers should be case insensitive anyways... #headers = normalize_headers(headers) unless headers.empty? # fhir_headers = convert_symbol_headers(fhir_headers) headers = convert_symbol_headers(headers) # supplied headers will always be used, e.g. if @use_accept_header is false # ,but an accept header is explicitly supplied then it will be used (or override the existing) fhir_headers.merge!(headers) unless headers.empty? fhir_headers.merge!(additional_headers) if /\Aapplication\/(fhir\+xml|xml\+fhir|fhir\+json|json\+fhir)\z/.match? fhir_headers['Content-Type'] fhir_headers['Content-Type'] = "#{fhir_headers['Content-Type']};charset=#{DEFAULT_CHARSET}" end fhir_headers end def self.resource_url(options, use_format_param = false) options = DEFAULTS.merge(options) params = {} url = '' # handle requests for resources by class or string; useful for testing nonexistent resource types url += "/#{options[:resource].try(:name).try(:demodulize) || options[:resource].split('::').last}" if options[:resource] url += "/#{options[:id]}" if options[:id] url += '/$validate' if options[:validate] url += '/$match' if options[:match] if options[:operation] opr = options[:operation] p = opr[:parameters] p = p.each { |k, v| p[k] = v[:value] } if p params.merge!(p) if p && opr[:method] == 'GET' if opr[:name] == :fetch_patient_record url += '/$everything' elsif opr[:name] == :value_set_expansion url += '/$expand' elsif opr && opr[:name] == :value_set_based_validation url += '/$validate-code' elsif opr && opr[:name] == :code_system_lookup url += '/$lookup' elsif opr && opr[:name] == :concept_map_translate url += '/$translate' elsif opr && opr[:name] == :closure_table_maintenance url += '/$closure' end end if options[:history] history = options[:history] url += '/_history' url += "/#{history[:id]}" if history.key?(:id) params[:_count] = history[:count] if history[:count] params[:_since] = history[:since].iso8601 if history[:since] end if options[:search] search_options = options[:search] url += '/_search' if search_options[:flag] url += "/#{search_options[:compartment]}" if search_options[:compartment] if search_options[:parameters] search_options[:parameters].each do |key, value| params[key.to_sym] = value end end end # options[:params] is simply appended at the end of a url and is used by testscripts url += options[:params] if options[:params] params[:_summary] = options[:summary] if options[:summary] if use_format_param && options[:format] params[:_format] = options[:format] end uri = Addressable::URI.parse(url) # params passed in options takes precidence over params calculated in this method # for use by testscript primarily uri.query_values = params unless options[:params] && options[:params].include?('?') uri.normalize.to_str end # Get the resource ID out of the URL (e.g. Bundle.entry.response.location) def self.pull_out_id(resource_type, url) id = nil if !resource_type.nil? && !url.nil? token = "#{resource_type}/" if url.index(token) start = url.index(token) + token.length t = url[start..-1] stop = (t.index('/') || 0) - 1 stop = -1 if stop.nil? id = t[0..stop] else id = nil end end id end def self.append_forward_slash_to_path(path) path += '/' unless path.last == '/' path end end end