openapi-sourcetools 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ module OpenAPISourceTools
7
+ # To hold documents loaded via command-line.
8
+ # Provides attribute accessor methods for each document.
9
+ # Exposed via Gen.d to tasks.
10
+ class Docs
11
+ attr_reader :docs
12
+
13
+ def initialize
14
+ @docs = {}
15
+ end
16
+
17
+ def method_missing(method_name, *args)
18
+ name = method_name.to_s
19
+ if name.end_with?('=')
20
+ name = name[0...(name.size - 1)]
21
+ super unless @docs.key?(name)
22
+ @docs[name] = args.first
23
+ return args.first
24
+ end
25
+ super unless @docs.key?(name)
26
+ @docs[name]
27
+ end
28
+
29
+ def respond_to_missing?(method_name, *args)
30
+ name = method_name.to_s
31
+ name = name[0...(name.size - 1)] if name.end_with?('=')
32
+ @docs.key?(name) || super
33
+ end
34
+
35
+ def add(name, content)
36
+ return false if docs.key?(name)
37
+ @docs[name] = content
38
+ true
39
+ end
40
+ end
41
+ end
@@ -1,37 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright © 2024 Ismo Kärkkäinen
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
4
  # Licensed under Universal Permissive License. See LICENSE.txt.
5
5
 
6
6
  require_relative 'task'
7
7
  require_relative 'helper'
8
8
  require_relative 'docs'
9
9
  require_relative 'output'
10
+ require_relative 'config'
11
+ require 'deep_merge'
10
12
 
11
13
 
14
+ # The generation module that contains things visible to tasks.
12
15
  module Gen
13
16
  def self.add_doc(symbol, docstr)
14
17
  return if docstr.nil?
15
18
  @docsrc = [] unless instance_variable_defined?('@docsrc')
16
- @docsrc.push("- #{symbol.to_s} : #{docstr}")
19
+ @docsrc.push("- #{symbol} : #{docstr}")
17
20
  end
21
+ private_class_method :add_doc
18
22
 
19
23
  def self.read_attr(symbol, default)
20
24
  return if symbol.nil?
21
25
  attr_reader(symbol)
22
26
  module_function(symbol)
23
- instance_variable_set("@#{symbol.to_s}", default)
27
+ instance_variable_set("@#{symbol}", default)
24
28
  end
29
+ private_class_method :read_attr
25
30
 
26
31
  def self.mod_attr2_reader(symbol, symbol2, docstr = nil, default = nil)
27
32
  read_attr(symbol, default)
28
33
  read_attr(symbol2, default)
29
34
  add_doc(symbol, docstr)
30
35
  end
36
+ private_class_method :mod_attr2_reader
31
37
 
32
38
  def self.mod_attr_reader(symbol, docstr = nil, default = nil)
33
39
  mod_attr2_reader(symbol, nil, docstr, default)
34
40
  end
41
+ private_class_method :mod_attr_reader
35
42
 
36
43
  def self.rw_attr(symbol, default)
37
44
  attr_accessor(symbol)
@@ -40,39 +47,56 @@ module Gen
40
47
  module_function((s + '=').to_sym)
41
48
  instance_variable_set("@#{s}", default)
42
49
  end
50
+ private_class_method :rw_attr
43
51
 
44
52
  def self.mod_attr2_accessor(symbol, symbol2, docstr = nil, default = nil)
45
53
  rw_attr(symbol, default)
46
54
  rw_attr(symbol2, default) unless symbol2.nil?
47
55
  add_doc(symbol, docstr)
48
56
  end
57
+ private_class_method :mod_attr2_accessor
49
58
 
50
59
  def self.mod_attr_accessor(symbol, docstr = nil, default = nil)
51
60
  mod_attr2_accessor(symbol, nil, docstr, default)
52
61
  end
62
+ private_class_method :mod_attr_accessor
53
63
 
54
64
  mod_attr_reader :doc, 'OpenAPI document.'
55
65
  mod_attr_reader :outdir, 'Output directory name.'
56
- mod_attr_reader :d, 'Other documents object.', Docs.new
66
+ mod_attr_reader :d, 'Other documents object.', OpenAPISourceTools::Docs.new
67
+ mod_attr_reader :wd, 'Original working directory', Dir.pwd
68
+ mod_attr_reader :configuration, 'Generator internal configuration'
69
+ mod_attr_accessor :config, 'Configuration file name for next gem or Ruby file.'
70
+ mod_attr_accessor :separator, 'Key separator in config file names.', nil
57
71
  mod_attr_accessor :in_name, 'OpenAPI document name, nil if stdin.'
58
72
  mod_attr_accessor :in_basename, 'OpenAPI document basename, nil if stdin.'
59
- mod_attr_accessor :tasks, 'Tasks array.', []
60
- mod_attr_accessor :g, 'Hash for storing values visible to all tasks.', {}
61
- mod_attr_accessor :a, 'Intended for instance with defined attributes.'
73
+ mod_attr_reader :g, 'Hash for storing values visible to all tasks.', {}
74
+ mod_attr_accessor :x, 'Hash for storing values visible to tasks from processor.', {}
62
75
  mod_attr_accessor :h, 'Instance of class with helper methods.'
76
+ mod_attr_accessor :tasks, 'Tasks array.', []
63
77
  mod_attr2_accessor :task, :t, 'Current task instance.'
64
78
  mod_attr_accessor :task_index, 'Current task index.'
65
- mod_attr_accessor :loaders, 'Array of generator loader methods.', []
66
- mod_attr2_accessor :output, :o, 'Output-related methods.', Output.new
79
+ mod_attr_accessor :loaders, 'Array of processor loader methods.', []
80
+ mod_attr_accessor :output, 'Output-formatting helper.', OpenAPISourceTools::Output.new
81
+
82
+ def self.load_config(config_prefix)
83
+ cfg = {}
84
+ cfgs = OpenAPISourceTools::ConfigLoader.find_files(name_prefix: config_prefix)
85
+ cfgs = OpenAPISourceTools::ConfigLoader.read_contents(cfgs)
86
+ cfgs.each { |c| cfg.deep_merge!(c) }
87
+ cfg
88
+ end
89
+ private_class_method :load_config
67
90
 
68
- def self.setup(document_content, input_name, output_directory)
91
+ def self.setup(document_content, input_name, output_directory, config_prefix)
69
92
  @doc = document_content
70
93
  @outdir = output_directory
71
94
  unless input_name.nil?
72
95
  @in_name = File.basename(input_name)
73
96
  @in_basename = File.basename(input_name, '.*')
74
97
  end
75
- add_task(task: HelperTask.new)
98
+ @configuration = load_config(config_prefix)
99
+ add_task(task: OpenAPISourceTools::HelperTask.new)
76
100
  end
77
101
 
78
102
  def self.add_task(task:, name: nil, executable: false, x: nil)
@@ -85,11 +109,12 @@ module Gen
85
109
  end
86
110
 
87
111
  def self.add_write_content(name:, content:, executable: false)
88
- add_task(task: WriteTask.new(name, content, executable))
112
+ add_task(task: OpenAPISourceTools::WriteTask.new(name, content, executable))
89
113
  end
90
114
 
91
115
  def self.add(source:, template: nil, template_name: nil, name: nil, executable: false, x: nil)
92
- add_task(task: Task.new(source, template, template_name), name: name, executable: executable, x: x)
116
+ add_task(task: OpenAPISourceTools::Task.new(source, template, template_name),
117
+ name:, executable:, x:)
93
118
  end
94
119
 
95
120
  def self.document
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024-2025 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative 'common'
8
+ require_relative 'loaders'
9
+ require_relative 'gen'
10
+
11
+
12
+ module OpenAPISourceTools
13
+ def self.executable_bits_on(mode)
14
+ mode = mode.to_s(8).chars
15
+ mode.size.times do |k|
16
+ m = mode[k].to_i(8)
17
+ # Applies to Unix-likes. Other system, check and handle.
18
+ m += 1 unless 3 < mode.size - k || m.zero? || m.odd?
19
+ mode[k] = m
20
+ end
21
+ m = 0
22
+ mode.each do |v|
23
+ m = 8 * m + v
24
+ end
25
+ m
26
+ end
27
+
28
+ # Runs all tasks that generate the results.
29
+ # Used internally by openapi-generate.
30
+ class Generator
31
+ def initialize(document_content, input_name, output_directory, config_prefix)
32
+ Gen.setup(document_content, input_name, output_directory, config_prefix)
33
+ Gen.loaders = Loaders.loaders
34
+ end
35
+
36
+ def context_binding
37
+ binding
38
+ end
39
+
40
+ def load(generator_names)
41
+ generator_names.each do |name|
42
+ idx = Gen.loaders.index { |loader| loader.call(name) }
43
+ return Common.aargh("No loader could handle #{name}", 2) if idx.nil?
44
+ end
45
+ 0
46
+ rescue StandardError => e
47
+ Common.aargh(e.to_s, 2)
48
+ end
49
+
50
+ def generate(t)
51
+ t.generate(context_binding)
52
+ rescue Exception => e
53
+ Common.aargh(e.to_s, 4)
54
+ end
55
+
56
+ def output_name(t, index)
57
+ name = t.output_name
58
+ name = "#{index}.txt" if name.nil?
59
+ File.join(Gen.outdir, name)
60
+ end
61
+
62
+ def save(name, contents, executable)
63
+ d = File.dirname(name)
64
+ FileUtils.mkdir_p(d) unless File.directory?(d)
65
+ f = File.new(name, File::WRONLY | File::CREAT | File::TRUNC)
66
+ s = executable ? f.stat : nil
67
+ f.write(contents)
68
+ f.close
69
+ return unless executable
70
+ mode = OpenAPISourceTools.executable_bits_on(s.mode)
71
+ File.chmod(mode, name) unless mode == s.mode
72
+ end
73
+
74
+ def run
75
+ # This allows tasks to be added while processing.
76
+ # Not intended to be done but might prove handy.
77
+ # Also exposes current task index in case new task is added in the middle.
78
+ Gen.task_index = 0
79
+ while Gen.task_index < Gen.tasks.size
80
+ Gen.t = Gen.tasks[Gen.task_index]
81
+ Gen.task = Gen.t
82
+ out = generate(Gen.t)
83
+ Gen.task_index += 1
84
+ return out if out.is_a?(Integer)
85
+ next if Gen.t.discard || out.empty?
86
+ name = output_name(Gen.t, Gen.task_index - 1)
87
+ begin
88
+ save(name, out, Gen.t.executable)
89
+ rescue StandardError => e
90
+ return Common.aargh("Error writing output file: #{name}\n#{e}", 3)
91
+ end
92
+ end
93
+ 0
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+
8
+ module OpenAPISourceTools
9
+ # Helper class supposed to contain helpful methods.
10
+ # Exposed as Gen.h if HelperTask has been run. It is automatically
11
+ # added as the first task but later tasks can remove it.
12
+ class Helper
13
+ attr_reader :doc, :parents
14
+ attr_accessor :parent_parameters
15
+
16
+ # Stores the nearest Hash for each Hash.
17
+ def store_parents(obj, parent = nil)
18
+ if obj.is_a?(Hash)
19
+ @parents[obj] = parent
20
+ obj.each_value do |v|
21
+ store_parents(v, obj)
22
+ end
23
+ elsif obj.is_a?(Array)
24
+ obj.each do |v|
25
+ store_parents(v, parent)
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(doc)
31
+ @doc = doc
32
+ @parents = {}.compare_by_identity
33
+ store_parents(@doc)
34
+ end
35
+
36
+ def parent(object)
37
+ @parents[object]
38
+ end
39
+
40
+ COMPONENTS = '#/components/'
41
+
42
+ def category_and_name(ref_or_obj)
43
+ ref = ref_or_obj.is_a?(Hash) ? ref_or_obj['$ref'] : ref_or_obj
44
+ return nil unless ref.is_a?(String)
45
+ return nil unless ref.start_with?(Helper::COMPONENTS)
46
+ idx = ref.index('/', Helper::COMPONENTS.size)
47
+ return nil if idx.nil?
48
+ category = ref[Helper::COMPONENTS.size...idx]
49
+ [ category, ref[(idx + 1)...ref.size] ]
50
+ end
51
+
52
+ def dereference(ref_or_obj)
53
+ cn = category_and_name(ref_or_obj)
54
+ return nil if cn.nil?
55
+ cs = @doc.dig('components', cn.first) || {}
56
+ cs[cn.last]
57
+ end
58
+
59
+ def basename(ref_or_obj)
60
+ cn = category_and_name(ref_or_obj)
61
+ return nil if cn.nil?
62
+ cn.last
63
+ end
64
+
65
+ def parameters(operation_object, empty_unless_local = false)
66
+ return [] if empty_unless_local && !operation_object.key?('parameters')
67
+ cps = @doc.dig('components', 'parameters') || {}
68
+ uniqs = {}
69
+ path_item_object = parent(operation_object)
70
+ [path_item_object, operation_object].each do |p|
71
+ p.fetch('parameters', []).each do |param|
72
+ r = basename(param)
73
+ r = cps[r] if r.is_a?(String)
74
+ uniqs["#{r['name']}:#{r['in']}"] = param
75
+ end
76
+ end
77
+ uniqs.keys.sort!.map { |k| uniqs[k] }
78
+ end
79
+ end
80
+
81
+ # Task class to add an Helper instance to Gen.h, for convenience.
82
+ class HelperTask
83
+ include OpenAPISourceTools::TaskInterface
84
+
85
+ def generate(_context_binding)
86
+ Gen.h = Helper.new(Gen.doc) if Gen.h.nil?
87
+ end
88
+
89
+ def discard
90
+ true
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+
8
+
9
+ # Original loader functions. These are accessible via Gen.loaders. New loaders
10
+ # should be added there.
11
+ module OpenAPISourceTools
12
+ # Loaders used to load gems and files and set config etc.
13
+ # Exposed as Gen.loaders if you need to modify the array.
14
+ module Loaders
15
+ # Prefix etc. and loader pairs for all loaders.
16
+
17
+ REQ_PREFIX = 'req:'
18
+
19
+ def self.req_loader(name)
20
+ return false unless name.downcase.start_with?(REQ_PREFIX)
21
+ begin
22
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
23
+ Gen.tasks.push(t)
24
+ base = name.slice(REQ_PREFIX.size...name.size)
25
+ require(base)
26
+ Gen.config = nil
27
+ t.x = Gen.x # In case setup code replaced the object.
28
+ rescue LoadError => e
29
+ raise StandardError, "Failed to require #{name}\n#{e}"
30
+ rescue Exception => e
31
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
32
+ end
33
+ true
34
+ end
35
+
36
+ REREQ_PREFIX = 'rereq:'
37
+
38
+ def self.rereq_loader(name)
39
+ return false unless name.downcase.start_with?(REREQ_PREFIX)
40
+ begin
41
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
42
+ Gen.tasks.push(t)
43
+ code = name.slice(REREQ_PREFIX.size...name.size)
44
+ eval(code)
45
+ Gen.config = nil
46
+ t.x = Gen.x # In case setup code replaced the object.
47
+ rescue LoadError => e
48
+ raise StandardError, "Failed to require again #{name}\n#{e}"
49
+ rescue Exception => e
50
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
51
+ end
52
+ true
53
+ end
54
+
55
+ RUBY_EXT = '.rb'
56
+
57
+ def self.ruby_loader(name)
58
+ return false unless name.downcase.end_with?(RUBY_EXT)
59
+ origwd = Dir.pwd
60
+ d = File.dirname(name)
61
+ Dir.chdir(d) unless d == '.'
62
+ begin
63
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
64
+ Gen.tasks.push(t)
65
+ base = File.basename(name)
66
+ Gen.config = base[0..-4] if Gen.config.nil?
67
+ require(File.join(Dir.pwd, base))
68
+ Gen.config = nil
69
+ t.x = Gen.x # In case setup code replaced the object.
70
+ rescue LoadError => e
71
+ raise StandardError, "Failed to require #{name}\n#{e}"
72
+ rescue Exception => e
73
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
74
+ end
75
+ Dir.chdir(origwd) unless d == '.'
76
+ true
77
+ end
78
+
79
+ YAML_PREFIX = 'yaml:'
80
+ YAML_EXTS = [ '.yaml', '.yml' ].freeze
81
+
82
+ def self.yaml_loader(name)
83
+ d = name.downcase
84
+ if d.start_with?(YAML_PREFIX)
85
+ name = name.slice(YAML_PREFIX.size...name.size)
86
+ elsif (YAML_EXTS.index { |s| d.end_with?(s) }).nil?
87
+ return false
88
+ end
89
+ n, _sep, f = name.partition(':')
90
+ raise StandardError, 'No name given.' if n.empty?
91
+ raise StandardError, 'No filename given.' if f.empty?
92
+ doc = YAML.safe_load_file(f)
93
+ raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
94
+ true
95
+ rescue Errno::ENOENT
96
+ raise StandardError, "Not found: #{f}\n#{e}"
97
+ rescue Exception => e # Whatever was raised, we want it.
98
+ raise StandardError, "Failed to read as YAML: #{f}\n#{e}"
99
+ end
100
+
101
+ BIN_PREFIX = 'bin:'
102
+
103
+ def self.bin_loader(name)
104
+ return false unless name.downcase.start_with?(BIN_PREFIX)
105
+ n, _sep, f = name.slice(BIN_PREFIX.size...name.size).partition(':')
106
+ raise StandardError, 'No name given.' if n.empty?
107
+ raise StandardError, 'No filename given.' if f.empty?
108
+ doc = File.binread(f)
109
+ raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
110
+ true
111
+ rescue Errno::ENOENT
112
+ raise StandardError, "Not found: #{f}\n#{e}"
113
+ rescue Exception => e # Whatever was raised, we want it.
114
+ raise StandardError, "Failed to read #{f}\n#{e}"
115
+ end
116
+
117
+ CONFIG_PREFIX = 'config:'
118
+
119
+ def self.config_loader(name)
120
+ return false unless name.downcase.start_with?(CONFIG_PREFIX)
121
+ raise StandardError, "Config name remains: #{Gen.config}" unless Gen.config.nil?
122
+ n = name.slice(CONFIG_PREFIX.size...name.size)
123
+ raise StandardError, 'No name given.' if n.empty?
124
+ # Interpretation left completely to config loading.
125
+ Gen.config = n
126
+ true
127
+ end
128
+
129
+ SEPARATOR_PREFIX = 'separator:'
130
+
131
+ def self.separator_loader(name)
132
+ return false unless name.downcase.start_with?(SEPARATOR_PREFIX)
133
+ n = name.slice(SEPARATOR_PREFIX.size...name.size)
134
+ n = nil if n.empty?
135
+ Gen.separator = n
136
+ true
137
+ end
138
+
139
+ def self.loaders
140
+ [
141
+ method(:req_loader),
142
+ method(:rereq_loader),
143
+ method(:ruby_loader),
144
+ method(:yaml_loader),
145
+ method(:bin_loader),
146
+ method(:config_loader),
147
+ method(:separator_loader)
148
+ ]
149
+ end
150
+
151
+ def self.document
152
+ <<EOB
153
+ - #{Loaders::REQ_PREFIX}req_name : requires the gem.
154
+ - #{Loaders::REREQ_PREFIX}code : runs code to add gem tasks again.
155
+ - ruby_file#{Loaders::RUBY_EXT} : changes to Ruby file directory and requires the file.
156
+ - #{Loaders::YAML_PREFIX}name:filename : Loads YAML file into Gen.d.name.
157
+ - name:filename.{#{(Loaders::YAML_EXTS.map { |s| s[1...s.size] }).join('|')}} : Loads YAML file into Gen.d.name.
158
+ - #{Loaders::BIN_PREFIX}name:filename : Loads binary file into Gen.d.name.
159
+ - #{Loaders::CONFIG_PREFIX}name : Sets Gen.config for next gem/Ruby file configuration loading.
160
+ - #{Loaders::SEPARATOR_PREFIX}string : Sets Gen.separator to string.
161
+ EOB
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+
7
+ module OpenAPISourceTools
8
+ # Output configuration settings for easy storage.
9
+ # You can have it in configuration and pass hash to initialize.
10
+ class OutputConfiguration
11
+ attr_reader :indent_character, :indent_step
12
+ attr_reader :tab, :tab_replaces_count
13
+
14
+ def initialize(cfg = {})
15
+ @indent_character = cfg['indent_character'] || ' '
16
+ @indent_step = cfg['indent_step'] || 4
17
+ @tab = cfg['tab'] || "\t"
18
+ @tab_replaces_count = cfg['tab_replaces_count'] || 0
19
+ end
20
+ end
21
+
22
+ # Output indentation helper class.
23
+ # Exposed as Gen.output for use from templates.
24
+ class Output
25
+ attr_reader :config
26
+ attr_accessor :last_indent
27
+
28
+ def initialize(cfg = OutputConfiguration.new)
29
+ @config = cfg
30
+ @last_indent = 0
31
+ end
32
+
33
+ def config=(cfg)
34
+ cfg = OutputConfiguration.new(cfg) if cfg.is_a?(Hash)
35
+ raise ArgumentError, "Expected OutputConfiguration or Hash, got #{cfg.class}" unless cfg.is_a?(OutputConfiguration)
36
+ @config = cfg
37
+ @last_indent = 0
38
+ end
39
+
40
+ # Takes an array of code blocks/lines or integers/booleans and produces
41
+ # indented output using the separator character.
42
+ # Set class attributes to obtain desired outcome.
43
+ def join(blocks, separator = "\n")
44
+ indented = []
45
+ blocks.flatten!
46
+ indent = 0
47
+ blocks.each do |block|
48
+ if block.nil?
49
+ indent = 0
50
+ elsif block.is_a?(Integer)
51
+ indent += block
52
+ elsif block.is_a?(TrueClass)
53
+ indent += @config.indent_step
54
+ elsif block.is_a?(FalseClass)
55
+ indent -= @config.indent_step
56
+ else
57
+ block = block.to_s unless block.is_a?(String)
58
+ if block.empty?
59
+ indented.push('')
60
+ next
61
+ end
62
+ if indent.zero?
63
+ indented.push(block)
64
+ next
65
+ end
66
+ if @config.tab_replaces_count.positive?
67
+ tabs = @config.tab * (indent / @config.tab_replaces_count)
68
+ chars = @config.indent_character * (indent % @config.tab_replaces_count)
69
+ else
70
+ tabs = ''
71
+ chars = @config.indent_character * indent
72
+ end
73
+ lines = block.lines(chomp: true)
74
+ lines.each do |line|
75
+ indented.push("#{tabs}#{chars}#{line}")
76
+ end
77
+ end
78
+ end
79
+ @last_indent = indent
80
+ indented.join(separator)
81
+ end
82
+ end
83
+ end