1module ConfigWriters
2  class Base
3    class Writer
4      def category(name)
5        raise NotImplementedError.new()
6      end
7    end
8
9    class TOMLWriter < Writer
10      PATH_DELIMITER = ".".freeze
11
12      def initialize
13        @indent = 0
14        @string = ""
15      end
16
17      def category(name)
18        last_line = @string.split("\n").last
19
20        if last_line && (last_line[0] != "[" || last_line[-1] != "]")
21          puts()
22        end
23
24        puts("# #{name}")
25      end
26
27      def hash(hash, path: [], tags: [])
28        if hash.length > 1
29          raise ArgumentError.new("A hash must contain only a single key and value")
30        end
31
32        key = nil
33        value = nil
34
35        if hash.values.first.is_a?(Hash)
36          hash = hash.flatten
37          key = hash.keys.first
38          value = hash.values.first
39        elsif hash.keys.first.include?(".")
40          key = hash.keys.first.inspect
41          value = hash.values.first
42        else
43          key = hash.keys.first
44          value = hash.values.first
45        end
46
47        kv(key, value, path: path, tags: tags)
48      end
49
50      def indent(spaces)
51        @indent += spaces
52      end
53
54      def kv(key, value, path: [], tags: [])
55        quoted_key = key.include?(" ") ? key.to_toml : key
56        full_key = (path + [quoted_key]).join(PATH_DELIMITER)
57        line = "#{full_key} = #{value.to_toml(hash_style: :inline)}"
58
59        if !line.include?("\n") && tags.any?
60          line << " # #{tags.join(", ")}"
61        end
62
63        puts(line)
64      end
65
66      def print(string)
67        @string << string
68      end
69
70      def puts(string = nil)
71        if string == nil
72          @string << "\n"
73        else
74          @string << ("#{string}".indent(@indent) + "\n")
75        end
76      end
77
78      def table(name, array: false, path: [])
79        full_name = (path + [name]).join(PATH_DELIMITER)
80
81        if array
82          puts("[[#{full_name}]]")
83        else
84          puts("[#{full_name}]")
85        end
86
87        indent(2)
88      end
89
90      def to_s
91        @string.rstrip
92      end
93    end
94
95    attr_reader :array, :block, :fields, :group, :key_path, :table_path, :values
96
97    def initialize(fields, array: false, group: nil, key_path: [], table_path: [], values: nil, &block)
98      if !fields.is_a?(Array)
99        raise ArgumentError.new("fields must be an array")
100      end
101
102      if block_given?
103        fields = fields.select(&block)
104      end
105
106      @array = array
107      @fields = fields
108      @group = group
109      @key_path = key_path
110      @table_path = table_path
111      @block = block
112      @values = values || {}
113    end
114
115    def categories
116      @categories ||= fields.collect(&:category).uniq
117    end
118
119    def to_toml(table_style: :normal)
120      raise NotImplementedError.new()
121    end
122
123    private
124      def build_child_writer(fields, array: false, group: nil, key_path: [], table_path: [], values: nil)
125        self.class.new(fields, array: array, group: group, key_path: key_path, table_path: table_path, values: values, &block)
126      end
127
128      def field_tags(field, default: true, enum: true, example: false, optionality: true, relevant_when: true, type: true, short: false, unit: true)
129        tags = []
130
131        if optionality
132          if field.required?
133            tags << "required"
134          else
135            tags << "optional"
136          end
137        end
138
139        if example
140          if field.default.nil? && (!field.enum || field.enum.keys.length > 1)
141            tags << "example"
142          end
143        end
144
145        if default
146          if !field.default.nil?
147            if short
148              tags << "default"
149            else
150              tags << "default: #{field.default.inspect}"
151            end
152          elsif field.optional?
153            tags << "no default"
154          end
155        end
156
157        if type
158          if short
159            tags << field.type
160          else
161            tags << "type: #{field.type}"
162          end
163        end
164
165        if unit && !field.unit.nil?
166          if short
167            tags << field.unit
168          else
169            tags << "unit: #{field.unit}"
170          end
171        end
172
173        if enum && field.enum
174          if short && field.enum.keys.length > 1
175            tags << "enum"
176          else
177            escaped_values = field.enum.keys.collect { |enum| enum.to_toml }
178            if escaped_values.length > 1
179              tags << "enum: #{escaped_values.to_sentence(two_words_connector: " or ")}"
180            else
181              tag = "must be: #{escaped_values.first}"
182              if field.optional?
183                tag << " (if supplied)"
184              end
185              tags << tag
186            end
187          end
188        end
189
190        if relevant_when && field.relevant_when
191          word = field.required? ? "required" : "relevant"
192          tag = "#{word} when #{field.relevant_when_kvs.to_sentence(two_words_connector: " or ")}"
193          tags << tag
194        end
195
196        tags
197      end
198
199      def full_path
200        table_path + key_path
201      end
202  end
203end
204