michaelengland/FlipTheSwitch

View on GitHub
lib/flip_the_switch/reader/features.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'json'
require 'json-schema'

module FlipTheSwitch
  module Reader
    class Features
      def initialize(input, environment)
        @input = input
        @environment = environment
      end

      def features
        raise Error::InvalidFile.new(input_file) unless valid_file?
        raise Error::InvalidEnvironment.new(environment) unless environments_by_name.has_key?(environment)
        inherited_environment(environment).features
      end

      private
      attr_reader :input, :environment

      INHERITS_KEY = 'inherits_from'
      ENABLED_KEY = 'enabled'
      DESCRIPTION_KEY = 'description'

      def inherited_environment(env_name)
        inherited_env = environments_by_name[env_name]
        if inherited_env.has_parent?
          merge_environments(inherited_env, inherited_environment(inherited_env.parent_name))
        else
          inherited_env
        end
      end

      def merge_environments(overriding_env, parent_env)
        Environment.new(
          overriding_env.name,
          merge_features(parent_env.features, overriding_env.features),
          overriding_env.parent_name
        )
      end

      def merge_features(parent_features, overriding_features)
        parent_features.inject([]) { |merged_features, parent_feature|
          overriding_feature = overriding_features.detect { |feature| feature.name == parent_feature.name }
          if overriding_feature
            merged_features.push(merge_feature(parent_feature, overriding_feature))
          else
            merged_features.push(parent_feature)
          end
        }
      end

      def merge_feature(parent_feature, child_feature)
        Feature.new(parent_feature.name,
          (child_feature.enabled != nil) ? child_feature.enabled : parent_feature.enabled,
          child_feature.description ? child_feature.description : parent_feature.description,
          sub_features(child_feature, parent_feature),
          child_feature.parent_name
        )
      end

      def sub_features(child_feature, parent_feature)
        if !child_feature.sub_features.empty?
          merge_sub_features(parent_feature.sub_features, child_feature.sub_features)
        else
          parent_feature.sub_features
        end
      end

      def merge_sub_features(parent_feature_sub_features, child_feature_sub_features)
        child_feature_sub_features.inject([]) { |merged_sub_features, child_sub_feature|
          merged_sub_features.push(merge_feature(related_parents_sub_feature(parent_feature_sub_features, child_sub_feature.name), child_sub_feature))
        }
      end

      def related_parents_sub_feature(parent_feature_sub_features, sub_feature_name)
        parent_feature_sub_features.detect { |parent_sub_feature|
          parent_sub_feature.name == sub_feature_name
        }
      end

      def environments_by_name
        parse.inject({}) { |hash, env|
          hash[env.name] = env
          hash
        }
      end

      def parse
        @parse ||= json.map { |env_name, env_info|
          parse_environment(env_name, env_info)
        }
      end

      def parse_environment(name, info)
        Environment.new(name, parse_environment_features(info), info[INHERITS_KEY])
      end

      def parse_environment_features(info)
        info.select { |key, _|
          key != INHERITS_KEY
        }.map { |feature_name, feature_info|
          parse_feature(feature_name, feature_info, nil)
        }
      end

      def parse_feature(name, info, parent_name)
        Feature.new(name, info.fetch(ENABLED_KEY), info[DESCRIPTION_KEY], parse_sub_features(info, name), parent_name)
      end

      def parse_sub_features(info, parent_name)
        info.select { |key, _| ![ENABLED_KEY, DESCRIPTION_KEY].include?(key) }.map { |sub_name, sub_info|
          parse_feature(sub_name, sub_info, parent_name)
        }
      end

      def json
        JSON.parse(input_file)
      end

      def input_file
        File.read(input)
      rescue SystemCallError => e
        raise Error::UnreadableFile.new(e)
      end

      def valid_file?
        JSON::Validator.validate(expected_schema, input_file)
      end

      def expected_schema
        {
          type: :object,
          additionalProperties: {
            type: :object,
            additionalProperties: {
              properties: {
                enabled: {
                  type: :boolean
                },
                description: {
                  type: :string
                },
                additionalProperties: {
                  properties: {
                    enabled: {
                      type: :boolean
                    },
                    description: {
                      type: :string
                    },
                    required: [
                      :enabled
                    ]
                  }
                }
              },
              required: [
                :enabled
              ]
            }
          }
        }
      end
    end
  end
end