app/services/data_sources/json_notification_data_source.rb

Summary

Maintainability
A
35 mins
Test Coverage
require_relative "notification_data_source"
require_relative "../../shared/logging_module"
require_relative "../../shared/json_convertible"
require_relative "../../shared/models/notification"

module FastlaneCI
  # Mixin the JSONConvertible class for Notification
  class Notification
    include FastlaneCI::JSONConvertible
  end

  # Data source for notifications backed by JSON
  class JSONNotificationDataSource < NotificationDataSource
    include FastlaneCI::JSONDataSource
    include FastlaneCI::Logging

    class << self
      attr_accessor :file_semaphore
    end

    # Can't have us reading and writing to a file at the same time
    JSONNotificationDataSource.file_semaphore = Mutex.new

    # Reloads notifications from the notifications data source after instantiation
    #
    # @param [Any] params
    def after_creation(**params)
      reload_notifications
    end

    # Returns an array of notifications from the notifications JSON file stored
    # in the notifications directory
    #
    # @return [Array[Notification]]
    def notifications
      JSONProjectDataSource.projects_file_semaphore.synchronize do
        return unless File.exist?(notifications_file_path)

        return JSON.parse(File.read(notifications_file_path))
                   .map(&Notification.method(:from_json!))
      end
    end

    # Writes the notifications array to the notifications directory as JSON
    #
    # @param  [Array[Notification]] notifications
    def notifications=(notifications)
      JSONNotificationDataSource.file_semaphore.synchronize do
        return unless File.exist?(notifications_file_path)

        File.write(
          notifications_file_path,
          JSON.pretty_generate(notifications.map(&:to_object_dictionary))
        )
      end
    end

    # Returns `true` if the notification exists in the in-memory notifications object
    #
    # @param  [String] id
    # @return [Boolean]
    def notification_exist?(id: nil)
      JSONNotificationDataSource.file_semaphore.synchronize do
        return @notifications.any? { |notification| notification.id == id }
      end
    end

    # Swaps the old notification record with the updated notification record if
    # the notification exists
    #
    # @param  [Notification] notification
    def update_notification!(notification: nil)
      notification.updated_at = Time.now

      notification_index = nil
      existing_notification = nil

      @notifications.each.with_index do |old_notification, index|
        if old_notification.id == notification.id
          notification_index = index
          existing_notification = old_notification
          break
        end
      end

      if existing_notification.nil?
        error_message = "Couldn't update notification #{notification.name} because it doesn't exist"
        logger.error(error_message)
        raise error_message
      else
        @notifications[notification_index] = notification
        self.notifications = @notifications
        path = notifications_file_path
        notification_name = existing_notification.name
        logger.debug("Updating notification #{notification_name}, writing out notifications.json to #{path}")
      end
    end

    # Creates and returns a new notification object. Writes said object to `notifications.json`
    #
    # @param  [String] id
    # @param  [String] priority
    # @param  [String] type
    # @param  [String] user_id
    # @param  [String] name
    # @param  [String] message
    # @param  [String] details
    # @return [Notification]
    def create_notification!(id: nil, priority: nil, type: nil, user_id: nil, name: nil, message: nil, details: nil)
      new_notification = Notification.new(
        priority: priority,
        type: type,
        user_id: user_id,
        name: name,
        message: message,
        details: details
      )

      if !notification_exist?(id: new_notification.id)
        self.notifications = @notifications.push(new_notification)
        logger.debug(
          "Added notification #{new_notification.name}, writing out notifications.json to #{notifications_file_path}"
        )
        return new_notification
      else
        logger.debug("Couldn't add notification #{notification.name} because it already exists")
        return nil
      end
    end

    # Deletes a notification if it matches the `id` passed in
    #
    # @param  [String] id
    def delete_notification!(id: nil)
      self.notifications = @notifications.delete_if { |notification| notification.id == id }
    end

    private

    # Returns the file path for the notifications to be read from / persisted to
    #
    #   ~/.fastlane/ci/notifications/notifications.json
    #
    # @param  [String] path
    # @return [String]
    def notifications_file_path(path: "notifications/notifications.json")
      return File.join(json_folder_path, path)
    end

    # Reloads the notifications from the data source
    def reload_notifications
      JSONNotificationDataSource.file_semaphore.synchronize do
        @notifications =
          if !File.exist?(notifications_file_path)
            notifications_dirname = File.dirname(notifications_file_path)
            FileUtils.mkdir_p(File.dirname(notifications_file_path)) unless Dir.exist?(notifications_dirname)
            File.open(File.expand_path(notifications_file_path), "w") do |file|
              file.write("[]")
            end
            []
          else
            JSON.parse(File.read(notifications_file_path)).map do |notification_object_hash|
              Notification.from_json!(notification_object_hash)
            end
          end
      end
    end
  end
end