ContentParser.decode_and_fetch_assets()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 12
ccs 6
cts 6
cp 1
crap 1
rs 9.8
c 0
b 0
f 0
1 1
require "json"
2 1
require "base64"
3 1
require_relative "file_fetcher"
4 1
require_relative "file_system"
5 1
require_relative "resume_node_types"
6
7 1
module Resume
8 1
  module CLI
9
    # Module concerned with parsing resume JSON objects and decoding
10
    # any encoded information within them.
11
    #
12
    # @author Paul Fioravanti
13 1
    module ContentParser
14 1
      ASSET_PATH = /dropbox/.freeze
15 1
      private_constant :ASSET_PATH
16
      # Regex taken from http://stackoverflow.com/q/8571501/567863
17
      BASE64_STRING_REGEX =
18 1
        %r{\A
19
        ([A-Za-z0-9+/]{4})*
20
        ([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)
21
        \z}x.freeze
22 1
      private_constant :BASE64_STRING_REGEX
23
24 1
      module_function
25
26
      # Decodes a Base64-encoded string into a UTF-8 string.
27
      #
28
      # @param string [String] a Base64-encoded string.
29
      # @return [String] a decoded UTF-8 string.
30 1
      def decode_content(string)
31
        # Force encoding to UTF-8 is needed for strings that had UTF-8
32
        # characters in them when they were originally encoded
33 202
        Base64.strict_decode64(string).force_encoding("utf-8")
34
      end
35
36
      # Parses a resume JSON file, decoding any encoded strings and fetching
37
      # any remote resume assets needed to build the resume.
38
      #
39
      # @param resume [Hash] The resume data hash.
40
      # @return [Hash] The parsed resume data hash.
41 1
      def parse(resume)
42 11
        JSON.recurse_proc(resume, &decode_and_fetch_assets)
43
      end
44
45
      # Values that need parsing can be found in hash and array values
46
      # in the JSON, so specifically target those data types for
47
      # manipulation, and ignore any direct references given to the
48
      # keys or values of the JSON hash.
49 1
      def decode_and_fetch_assets
50 11
        proc do |object|
51 1242
          case object
52
          when Hash
53 126
            parse_hash(object)
54
          when Array
55 87
            parse_array(object)
56
          else
57 1029
            object
58
          end
59
        end
60
      end
61 1
      private_class_method :decode_and_fetch_assets
62
63 1
      def parse_hash(hash)
64 126
        hash.each do |key, value|
65 564
          if value.is_a?(String)
66 182
            if value.match?(BASE64_STRING_REGEX)
67 140
              value = decode_content(value)
68
            end
69 182
            if value.match?(ASSET_PATH)
70 27
              hash[key] = FileFetcher.fetch(value)
71 27
              next
72
            end
73
          end
74 537
          munge_hash_value(hash, key, value)
75
        end
76
      end
77 1
      private_class_method :parse_hash
78
79 1
      def munge_hash_value(hash, key, value)
80 537
        case Hash[key, value]
81
        when ResumeNodeTypes::AlignValue
82
          # Prawn specifically requires :align values to
83
          # be symbols otherwise it errors out
84 14
          hash[key] = value.to_sym
85
        when ResumeNodeTypes::FontHash
86
          # This is the hash that tells Prawn what the fonts to be used
87
          # are called and where they are located
88 2
          substitute_filenames_for_filepaths(value)
89
        when ResumeNodeTypes::StylesArray
90
          # Prawn specifically requires :styles values to
91
          # be symbols otherwise the styles do not take effect
92 51
          hash[key] = value.map!(&:to_sym)
93
        else
94 470
          hash[key] = value
95
        end
96
      end
97 1
      private_class_method :munge_hash_value
98
99 1
      def substitute_filenames_for_filepaths(value)
100 2
        %i[normal bold].each do |property|
101 4
          if value.key?(property)
102 2
            value[property] = FileSystem.tmpfile_path(value[property])
103
          end
104
        end
105
      end
106 1
      private_class_method :substitute_filenames_for_filepaths
107
108 1
      def parse_array(array)
109 87
        array.each_with_index do |value, index|
110 103
          if value.is_a?(String) && value.match?(BASE64_STRING_REGEX)
111 58
            array[index] = decode_content(value)
112
          end
113
        end
114
      end
115 1
      private_class_method :parse_array
116
    end
117
  end
118
end
119