maestrano/maestrano-connector-rails

View on GitHub
app/models/maestrano/connector/rails/concerns/connec_helper.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Maestrano::Connector::Rails::Concerns::ConnecHelper
  extend ActiveSupport::Concern

  module ClassMethods
    def dependancies
      # Meant to be overloaded if needed
      {
        connec: '1.0',
        impac: '1.0',
        maestrano_hub: '1.0'
      }
    end

    def get_client(organization)
      client = Maestrano::Connec::Client[organization.tenant].new(organization.uid)
      client.class.headers('CONNEC-EXTERNAL-IDS' => 'true')
      client
    end

    # Returns a string of the tenant's current connec version.
    # Can use Gem::Version for version comparison
    def connec_version(organization)
      @@connec_version = Rails.cache.fetch("connec_version_#{organization.tenant}", namespace: 'maestrano', expires_in: 1.day) do
        response = get_client(organization).class.get("#{Maestrano[organization.tenant].param('connec.host')}/version", headers: {'Accept' => 'application/json'})
        response = JSON.parse(response.body)
        @@connec_version = response['ci_branch'].delete('v')
      end
      @@connec_version
    end

    def connec_version_lt?(version, organization)
      version = Gem::Version.new(version)
      current_version = Gem::Version.new(connec_version(organization))

      current_version < version
    rescue
      true
    end

    # Replaces the arrays of id received from Connec! by the id of the external application
    # Returns a hash {entity: {}, connec_id: '', id_refs_only_connec_entity: {}}
    # If an array has no id for this oauth_provider and oauth_uid but has one for connec, it returns a nil entity (skip the record)
    def unfold_references(connec_entity, references, organization)
      references = format_references(references)
      unfolded_connec_entity = connec_entity.deep_dup.with_indifferent_access
      not_nil = true

      # Id
      id_hash = unfolded_connec_entity['id'].find { |id| id['provider'] == organization.oauth_provider && id['realm'] == organization.oauth_uid }
      connec_id = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id']
      unfolded_connec_entity['id'] = id_hash ? id_hash['id'] : nil

      # Other references
      # Record references are references to other records (organization_id, item_id, ...)
      references[:record_references].each do |reference|
        not_nil &= unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization)
      end
      # Id references are references to sub entities ids (invoice lines id, ...)
      # We do not return nil if we're missing an id reference
      references[:id_references].each do |reference|
        unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization)
      end
      unfolded_connec_entity = not_nil ? unfolded_connec_entity : nil

      # Filter the connec entity to keep only the id_references fields (in order to save some memory)
      # Give an empty hash if there's nothing left
      id_refs_only_connec_entity = filter_connec_entity_for_id_refs(connec_entity, references[:id_references])

      {entity: unfolded_connec_entity, connec_id: connec_id, id_refs_only_connec_entity: id_refs_only_connec_entity}
    end

    # Replaces ids from the external application by arrays containing them
    def fold_references(mapped_external_entity, references, organization)
      references = format_references(references)
      mapped_external_entity = mapped_external_entity.with_indifferent_access

      # Use both record_references and id_references + the id
      (references.values.flatten + ['id']).each do |reference|
        fold_references_helper(mapped_external_entity, reference.split('/'), organization)
      end

      mapped_external_entity
    end

    # Builds an id_hash from the id and organization
    def id_hash(id, organization)
      {
        id: id,
        provider: organization.oauth_provider,
        realm: organization.oauth_uid
      }
    end

    # Recursive method for folding references
    def fold_references_helper(entity, array_of_refs, organization)
      ref = array_of_refs.shift
      field = entity[ref]
      return if field.blank?

      # Follow embedment path, remplace if it's not an array or a hash
      case field
      when Array
        field.each do |f|
          fold_references_helper(f, array_of_refs.dup, organization)
        end
      when Hash
        fold_references_helper(entity[ref], array_of_refs, organization)
      else
        id = field
        entity[ref] = [id_hash(id, organization)]
      end
    end

    # Recursive method for unfolding references
    def unfold_references_helper(entity, array_of_refs, organization)
      ref = array_of_refs.shift
      field = entity[ref]

      # Unfold the id
      if array_of_refs.empty? && field
        return entity.delete(ref) if field.is_a?(String) # ~retro-compatibility to ease transition aroud Connec! idmaps rework. Should be removed eventually.

        id_hash = field.find { |id| id[:provider] == organization.oauth_provider && id[:realm] == organization.oauth_uid }
        if id_hash
          entity[ref] = id_hash['id']
        elsif field.find { |id| id[:provider] == 'connec' } # Should always be true as ids will always contain a connec id
          # We may enqueue a fetch on the endpoint of the missing association, followed by a re-fetch on this one.
          # However it's expected to be an edge case, so for now we rely on the fact that the webhooks should be relativly in order.
          # Worst case it'll be done on following sync
          entity.delete(ref)
          return nil
        end
        true

      # Follow embedment path
      else
        return true if field.blank?

        case field
        when Array
          bool = true
          field.each do |f|
            bool &= unfold_references_helper(f, array_of_refs.dup, organization)
          end
          bool
        when Hash
          unfold_references_helper(entity[ref], array_of_refs, organization)
        end
      end
    end

    # Transforms the references into an hash {record_references: [], id_references: []}
    # References can either be an array (only record references), or a hash
    def format_references(references)
      return {record_references: references, id_references: []} if references.is_a?(Array)

      references[:record_references] ||= []
      references[:id_references] ||= []
      references
    end

    # Returns the connec_entity without all the fields that are not id_references
    def filter_connec_entity_for_id_refs(connec_entity, id_references)
      return {} if id_references.empty?

      entity = connec_entity.dup.with_indifferent_access
      tree = build_id_references_tree(id_references)

      filter_connec_entity_for_id_refs_helper(entity, tree)

      # TODO, improve performance by returning an empty hash if all the id_references have their id in the connec hash
      # We should still return all of them if at least one is missing as we are relying on the id
      entity
    end

    # Recursive method for filtering connec entities
    def filter_connec_entity_for_id_refs_helper(entity_hash, tree)
      return if tree.empty?

      entity_hash.slice!(*tree.keys)

      tree.each do |key, children|
        case entity_hash[key]
        when Array
          entity_hash[key].each do |hash|
            filter_connec_entity_for_id_refs_helper(hash, children)
          end
        when Hash
          filter_connec_entity_for_id_refs_helper(entity_hash[key], children)
        end
      end
    end

    # Builds a tree from an array of id_references
    # input: %w(lines/id lines/linked/id linked/id)
    # output: {"lines"=>{"id"=>{}, "linked"=>{"id"=>{}}}, "linked"=>{"id"=>{}}}
    def build_id_references_tree(id_references)
      tree = {}

      id_references.each do |id_reference|
        array_of_refs = id_reference.split('/')

        t = tree
        array_of_refs.each do |ref|
          t[ref] ||= {}
          t = t[ref]
        end
      end

      tree
    end

    # Merges the id arrays from two hashes while keeping only the id_references fields
    def merge_id_hashes(dist, src, id_references)
      dist = dist.with_indifferent_access
      src = src.with_indifferent_access

      id_references.each do |id_reference|
        array_of_refs = id_reference.split('/')

        merge_id_hashes_helper(dist, array_of_refs, src)
      end

      dist
    end

    # Recursive helper for merging id hashes
    def merge_id_hashes_helper(hash, array_of_refs, src, path = [])
      ref = array_of_refs.shift
      field = hash[ref]

      if array_of_refs.empty? && field
        value = value_from_hash(src, path + [ref])
        if value.is_a?(Array)
          hash[ref] = (field + value).uniq
        else
          hash.delete(ref)
        end
      else
        case field
        when Array
          field.each_with_index do |f, index|
            merge_id_hashes_helper(f, array_of_refs.dup, src, path + [ref, index])
          end
        when Hash
          merge_id_hashes_helper(field, array_of_refs, src, path + [ref])
        end
      end
    end

    # Returns the value from a hash following the given path
    # Path sould be an array like [:lines, 0, :id]
    def value_from_hash(hash, path)
      value = hash

      begin
        path.each do |p|
          value = value[p]
        end
        value
      rescue NoMethodError
        nil
      end
    end
  end
end