class Holidays::Definition::Context::Generator

Public Class Methods

new(custom_method_parser, custom_method_source_decorator, custom_methods_repository, test_parser, test_source_generator, module_source_generator) click to toggle source
# File lib/holidays/definition/context/generator.rb, line 14
def initialize(custom_method_parser, custom_method_source_decorator, custom_methods_repository, test_parser, test_source_generator, module_source_generator)
  @custom_method_parser = custom_method_parser
  @custom_method_source_decorator = custom_method_source_decorator
  @custom_methods_repository = custom_methods_repository
  @test_parser = test_parser
  @test_source_generator = test_source_generator
  @module_source_generator = module_source_generator
end

Public Instance Methods

generate_definition_source(module_name, files, regions, rules_by_month, custom_methods, tests) click to toggle source
# File lib/holidays/definition/context/generator.rb, line 61
def generate_definition_source(module_name, files, regions, rules_by_month, custom_methods, tests)
  month_strings = generate_month_definition_strings(rules_by_month, custom_methods)

  # Build the custom methods string
  custom_method_string = ''
  custom_methods.each do |key, code|
    custom_method_string << @custom_method_source_decorator.call(code) + ",\n\n"
  end

  module_src = @module_source_generator.call(module_name, files, regions, month_strings, custom_method_string)
  test_src = @test_source_generator.call(module_name, files, tests)

  return module_src, test_src || ''
end
parse_definition_files(files) click to toggle source
# File lib/holidays/definition/context/generator.rb, line 23
def parse_definition_files(files)
  raise ArgumentError, "Must have at least one file to parse" if files.nil? || files.empty?

  all_regions = []
  all_rules_by_month = {}
  all_custom_methods = {}
  all_tests = []

  files.flatten!

  files.each do |file|
    definition_file = YAML.load_file(file)

    custom_methods = @custom_method_parser.call(definition_file['methods'])

    regions, rules_by_month = parse_month_definitions(definition_file['months'], custom_methods)

    all_regions << regions.flatten

    all_rules_by_month.merge!(rules_by_month) { |month, existing, new|
      existing << new
      existing.flatten!
    }

    #FIXME This is a problem. We will have a 'global' list of methods. That's always bad. What effects will this have?
    # This is an existing problem (just so we are clear). An issue would be extremely rare because we are generally parsing
    # single files/custom files. But it IS possible that we would parse a bunch of things at the same time and step
    # on each other so we need a solution.
    all_custom_methods.merge!(custom_methods)

    all_tests += @test_parser.call(definition_file['tests'])
  end

  all_regions.flatten!.uniq!

  [all_regions, all_rules_by_month, all_custom_methods, all_tests]
end

Private Instance Methods

generate_month_definition_strings(rules_by_month, parsed_custom_methods) click to toggle source

FIXME This should really be split out and tested with its own unit tests.

# File lib/holidays/definition/context/generator.rb, line 136
def generate_month_definition_strings(rules_by_month, parsed_custom_methods)
  month_strings = []

  rules_by_month.each do |month, rules|
    month_string = "      #{month.to_s} => ["
    rule_strings = []
    rules.each do |rule|
      string = '{'
      if rule[:mday]
        string << ":mday => #{rule[:mday]}, "
      end

      if rule[:function]
        string << ":function => \"#{rule[:function].to_s}\", "

        # We need to add in the arguments so we can know what to send in when calling the custom proc during holiday lookups.
        # NOTE: the allowed arguments are enforced in the custom methods parser.
        string << ":function_arguments => #{get_function_arguments(rule[:function], parsed_custom_methods)}, "

        if rule[:function_modifier]
          string << ":function_modifier => #{rule[:function_modifier].to_s}, "
        end
      end

      # This is the 'else'. It is possible for mday AND function
      # to be set but this is the fallback. This whole area
      # needs to be reworked!
      if string == '{'
        string << ":wday => #{rule[:wday]}, :week => #{rule[:week]}, "
      end

      if rule[:year_ranges] && rule[:year_ranges].is_a?(Hash)
        selector = rule[:year_ranges].keys.first
        value = rule[:year_ranges][selector]

        string << ":year_ranges => { :#{selector} => #{value} },"
      end

      if rule[:observed]
        string << ":observed => \"#{rule[:observed].to_s}\", "
        string << ":observed_arguments => #{get_function_arguments(rule[:observed], parsed_custom_methods)}, "
      end

      if rule[:type]
        string << ":type => :#{rule[:type]}, "
      end

      # shouldn't allow the same region twice
      string << ":name => \"#{rule[:name]}\", :regions => [:" + rule[:regions].uniq.join(', :') + "]}"
      rule_strings << string
    end
    month_string << rule_strings.join(",\n            ") + "]"
    month_strings << month_string
  end

  return month_strings
end
get_function_arguments(function_id, parsed_custom_methods) click to toggle source

This method sucks. The issue here is that the custom methods repo has the ‘general’ methods (like easter) but the ‘parsed_custom_methods’ have the recently parsed stuff. We don’t load those until they are needed later. This entire file is a refactor target so I am adding some tech debt to get me over the hump. What we should do is ensure that all custom methods are loaded into the repo as soon as they are parsed so we only have one place to look.

# File lib/holidays/definition/context/generator.rb, line 199
def get_function_arguments(function_id, parsed_custom_methods)
  if method = @custom_methods_repository.find(function_id)
    method.parameters.collect { |arg| arg[1] }
  elsif method = parsed_custom_methods[function_id]
    method.arguments.collect { |arg| arg.to_sym }
  end
end
parse_month_definitions(month_definitions, parsed_custom_methods) click to toggle source

FIXME This should be a ‘month_definitions_parser’ like the above parser

# File lib/holidays/definition/context/generator.rb, line 79
def parse_month_definitions(month_definitions, parsed_custom_methods)
  regions = []
  rules_by_month = {}

  if month_definitions
    month_definitions.each do |month, definitions|
      rules_by_month[month] = [] unless rules_by_month[month]
      definitions.each do |definition|
        rule = {}

        definition.each do |key, val|
          # Ruby 2.4 doesn't have the `transform_keys` method. Once we drop 2.4 support we can
          # use `val.transform_keys!(&:to_sym) if val.is_a?(Hash)` instead of this `if` statement.
          if val.is_a?(Hash)
            val = val.keys.each_with_object({}) do |k, result|
              result[k.to_sym] = val[k]
            end
          end

          rule[key.to_sym] = val
        end

        if rule[:year_ranges] && rule[:year_ranges].key?(:between)
          start_year = rule[:year_ranges][:between]["start"].to_i
          end_year = rule[:year_ranges][:between]["end"].to_i

          rule[:year_ranges][:between] = Range.new(start_year, end_year)
        end

        rule[:regions] = rule[:regions].collect { |r| r.to_sym }
        regions << rule[:regions]

        exists = false
        rules_by_month[month].each do |ex|
          if ex[:name] == rule[:name] and ex[:wday] == rule[:wday] and ex[:mday] == rule[:mday] and ex[:week] == rule[:week] and ex[:type] == rule[:type] and ex[:function] == rule[:function] and ex[:observed] == rule[:observed] and ex[:year_ranges] == rule[:year_ranges]
            ex[:regions] << rule[:regions].flatten
            exists = true
          end
        end

        unless exists
          # This will add in the custom method arguments so they are immediately
          # available for 'on the fly' def loading.
          if rule[:function]
            rule[:function_arguments] = get_function_arguments(rule[:function], parsed_custom_methods)
          end

          rules_by_month[month] << rule
        end
      end
    end
  end

  [regions, rules_by_month]
end