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.
data/bin/openapi-merge CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021-2024 Ismo Kärkkäinen
4
+ # Copyright © 2021-2025 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common'
7
+ require_relative '../lib/openapi/sourcetools/common'
8
8
  require 'optparse'
9
- require 'yaml'
10
9
  require 'set'
10
+ include OpenAPISourceTools
11
11
 
12
12
  def raise_se(message)
13
13
  raise StandardError, message
@@ -52,7 +52,7 @@ end
52
52
 
53
53
  def gather_refs(doc, found)
54
54
  doc.each_pair do |key, value|
55
- if key == '$ref' && value.is_a?(String) && value.start_with?('#/components/')
55
+ if key == '$ref' # Trust all refs to be valid.
56
56
  found.add(value)
57
57
  elsif value.is_a? Hash
58
58
  gather_refs(value, found)
@@ -64,6 +64,13 @@ def gather_refs(doc, found)
64
64
  end
65
65
  end
66
66
 
67
+ def has_refd_anchor(item, refs)
68
+ return !((item.index { |v| has_refd_anchor(v, refs) }).nil?) if item.is_a?(Array)
69
+ return false unless item.is_a?(Hash)
70
+ return true if refs.member?('#' + item.fetch('$anchor', ''))
71
+ !((item.values.index { |v| has_refd_anchor(v, refs) }).nil?)
72
+ end
73
+
67
74
  def prune(merged)
68
75
  prev_refs = Set.new
69
76
  loop do # May have references from deleted so repeat until nothing deleted.
@@ -74,16 +81,16 @@ def prune(merged)
74
81
  refs.add("#/components/securitySchemes/#{key}")
75
82
  end
76
83
  end
84
+ # Add schema ref for all schemas that have referenced anchor somewhere.
85
+ (merged.dig(*%w[components schemas]) || {}).each do |name, schema|
86
+ refs.add("#/components/Schemas/#{name}") if has_refd_anchor(schema, refs)
87
+ end
77
88
  used = {}
78
89
  all = merged.fetch('components', {})
79
90
  refs.each do |ref|
80
91
  p = ref.split('/')
81
92
  p.shift(2)
82
- item = all
83
- p.each do |key|
84
- item = item.fetch(key, nil)
85
- break if item.nil?
86
- end
93
+ item = all.dig(*p)
87
94
  next if item.nil?
88
95
  sub = used
89
96
  p.each_index do |k|
@@ -105,7 +112,6 @@ def main
105
112
  output_file = nil
106
113
  keep = false
107
114
 
108
- ENV['POSIXLY_CORRECT'] = '1'
109
115
  parser = OptionParser.new do |opts|
110
116
  opts.summary_indent = ' '
111
117
  opts.summary_width = 26
@@ -130,7 +136,7 @@ allowed only to append to the merged document, not re-define anything in it.
130
136
  exit 0
131
137
  end
132
138
  end
133
- parser.parse!
139
+ parser.order!
134
140
 
135
141
  max_depths = Hash.new(0)
136
142
  max_depths['openapi'] = 1
@@ -143,16 +149,16 @@ allowed only to append to the merged document, not re-define anything in it.
143
149
  max_depths['tags'] = 1
144
150
  merged = {}
145
151
  ARGV.each do |filename|
146
- s = load_source(filename)
152
+ s = Common.load_source(filename)
147
153
  return 2 if s.nil?
148
154
  add_undefined(merged, s, filename, [], max_depths)
149
155
  rescue StandardError => e
150
- return aargh(e.to_s, 4)
156
+ return Common.aargh(e.to_s, 4)
151
157
  end
152
158
 
153
159
  prune(merged) unless keep
154
160
 
155
- dump_result(output_file, YAML.dump(merged), 3)
161
+ Common.dump_result(output_file, merged, 3)
156
162
  end
157
163
 
158
- exit(main) if (defined? $unit_test).nil?
164
+ exit(main) if defined?($unit_test).nil?
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2024 Ismo Kärkkäinen
4
+ # Copyright © 2024-2025 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common'
7
+ require_relative '../lib/openapi/sourcetools/common'
8
8
  require 'optparse'
9
- require 'yaml'
9
+ include OpenAPISourceTools
10
+
10
11
 
11
12
  def path2pieces(s)
12
- s.split('/').reject { |p| p.empty? }
13
+ s.split('/').reject &:empty?
13
14
  end
14
15
 
15
16
  def pieces2path(p)
@@ -39,18 +40,18 @@ end
39
40
 
40
41
  def add_op(s)
41
42
  s = path2pieces(s)
42
- Proc.new { |path| add(s, path) }
43
+ proc { |path| add(s, path) }
43
44
  end
44
45
 
45
46
  def remove_op(s)
46
47
  s = path2pieces(s)
47
- Proc.new { |path| remove(s, path) }
48
+ proc { |path| remove(s, path) }
48
49
  end
49
50
 
50
51
  def replace_op(orig, s)
51
52
  o = path2pieces(orig)
52
53
  s = path2pieces(s)
53
- Proc.new { |path| replace(o, s, path) }
54
+ proc { |path| replace(o, s, path) }
54
55
  end
55
56
 
56
57
  def perform_operations(paths, operations)
@@ -77,23 +78,23 @@ def main
77
78
  opts.separator ''
78
79
  opts.separator 'Options:'
79
80
  opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
80
- exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
81
+ exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
81
82
  input_name = f
82
83
  end
83
84
  opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
84
- exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
85
+ exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
85
86
  output_name = f
86
87
  end
87
88
  opts.on('-a', '--add STR', 'Add prefix STR to all paths.') do |s|
88
- exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
89
+ exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
89
90
  operations.push(add_op(s))
90
91
  end
91
92
  opts.on('-d', '--delete PREFIX', 'Delete PREFIX when present.') do |s|
92
- exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
93
+ exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
93
94
  operations.push(remove_op(s))
94
95
  end
95
96
  opts.on('-r', '--replace PREFIX STR', 'Replace PREFIX with STR when present.') do |s|
96
- exit(aargh('Empty string to replace.', 1)) if s.empty?
97
+ exit(Common.aargh('Empty string to replace.', 1)) if s.empty?
97
98
  orig = s
98
99
  end
99
100
  opts.on('-h', '--help', 'Print this help and exit.') do
@@ -106,18 +107,18 @@ STR and PREFIX are expected to be parts of a path surrounded by /.
106
107
  end
107
108
  end
108
109
  parser.order! do |s|
109
- exit(aargh("String without option: #{s}", 1)) if orig.nil?
110
+ exit(Common.aargh("String without option: #{s}", 1)) if orig.nil?
110
111
  operations.push(replace_op(orig, s))
111
112
  orig = nil
112
113
  end
113
114
 
114
- doc = load_source(input_name)
115
+ doc = Common.load_source(input_name)
115
116
  return 2 if doc.nil?
116
117
 
117
118
  p = perform_operations(doc.fetch('paths', {}), operations)
118
119
  doc['paths'] = p unless p.empty?
119
120
 
120
- dump_result(output_name, doc, 3)
121
+ Common.dump_result(output_name, doc, 3)
121
122
  end
122
123
 
123
- main if defined?($unit_test).nil?
124
+ exit(main) if defined?($unit_test).nil?
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021-2024 Ismo Kärkkäinen
4
+ # Copyright © 2021-2025 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common'
7
+ require_relative '../lib/openapi/sourcetools/apiobjects'
8
+ require_relative '../lib/openapi/sourcetools/common'
8
9
  require 'optparse'
9
- require 'yaml'
10
+ include OpenAPISourceTools
10
11
 
11
12
 
12
13
  def main
@@ -32,26 +33,22 @@ def main
32
33
  opts.on('-h', '--help', 'Print this help and exit.') do
33
34
  $stdout.puts %(#{opts}
34
35
 
35
- Processes API specification document path objects into form that is expected by
36
- later stage tools. Checks for paths that may be ambiguous.
36
+ Adds split path parts into API document path items under x-openapi-sourcetools
37
+ key. Checks for paths that may be ambiguous.
37
38
  )
38
39
  exit 0
39
40
  end
40
41
  end
41
- parser.parse!
42
+ parser.order!
42
43
 
43
- doc = load_source(input_name)
44
+ doc = Common.load_source(input_name)
44
45
  return 2 if doc.nil?
45
46
 
46
47
  processed = {}
47
- doc.fetch('paths', {}).each_pair do |path, value|
48
- parts = split_path(path, true)
49
- processed[path] = {
50
- 'parts' => parts,
51
- 'orig' => value,
52
- 'lookalike' => [],
53
- path: ServerPath.new(parts)
54
- }
48
+ doc.fetch('paths', {}).each do |path, item|
49
+ parts = Common.split_path(path, true)
50
+ item['x-openapi-sourcetools-parts'] = parts # Added to original path item.
51
+ processed[path] = ApiObjects::ServerPath.new(parts)
55
52
  end
56
53
 
57
54
  # Find lookalike sets.
@@ -63,22 +60,14 @@ later stage tools. Checks for paths that may be ambiguous.
63
60
  k.times do |n|
64
61
  pn = paths[n]
65
62
  b = processed[pn]
66
- next unless a[:path].compare(b[:path]).zero?
67
- a['lookalike'].push pn
68
- b['lookalike'].push pk
63
+ next unless a.compare(b).zero?
69
64
  $stderr.puts("Similar: #{pn} #{pk}")
70
65
  lookalikes = true
71
66
  end
72
67
  end
73
- return aargh('Similar paths found.', 4) if lookalikes && error
68
+ return Common.aargh('Similar paths found.', 4) if lookalikes && error
74
69
 
75
- # Remove temporary fields.
76
- processed.each_value do |v|
77
- v.keys.each { |k| v.delete(k) if k.is_a? Symbol }
78
- end
79
- doc['paths'] = processed
80
-
81
- dump_result(output_name, YAML.dump(doc), 3)
70
+ Common.dump_result(output_name, doc, 3)
82
71
  end
83
72
 
84
- exit(main) if (defined? $unit_test).nil?
73
+ exit(main) unless defined?($unit_test)
@@ -0,0 +1,191 @@
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
+ # Various classes for handling objects in the API specification.
8
+ # Used in various programs.
9
+ module ApiObjects
10
+
11
+ def self.same(a, b, ignored_keys = Set.new(%w[summary description]))
12
+ return a == b unless a.is_a?(Hash) && b.is_a?(Hash)
13
+ keys = Set.new(a.keys + b.keys) - ignored_keys
14
+ keys.to_a.each do |k|
15
+ return false unless a.key?(k) && b.key?(k)
16
+ return false unless same(a[k], b[k], ignored_keys)
17
+ end
18
+ true
19
+ end
20
+
21
+ def self.ref_string(name, schema_path)
22
+ "#{schema_path}/#{name}"
23
+ end
24
+
25
+ def self.reference(obj, schemas, schema_path, ignored_keys = Set.new(%w[summary description]), prefix = 'Schema')
26
+ # Check if identical schema has been added and if so, return the $ref string.
27
+ schemas.keys.sort.each do |k|
28
+ return ref_string(k, schema_path) if same(obj, schemas[k], ignored_keys)
29
+ end
30
+ # One of the numbers will not match existing keys. More number than keys.
31
+ (schemas.size + 1).times do |n|
32
+ # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
33
+ k = "#{prefix}#{n}x"
34
+ next if schemas.key?(k)
35
+ schemas[k] = obj.merge
36
+ return ref_string(k, schema_path)
37
+ end
38
+ end
39
+
40
+ # A component in the API specification for reference and anchor handling.
41
+ class Components
42
+ attr_reader :path, :prefix, :anchor2ref, :schema_names
43
+ attr_accessor :items, :ignored_keys
44
+
45
+ def initialize(path, prefix, ignored_keys = %w[summary description examples example $anchor])
46
+ path = "#/#{path.join('/')}/" if path.is_a?(Array)
47
+ path = "#{path}/" unless path.end_with?('/')
48
+ @path = path
49
+ @prefix = prefix
50
+ @anchor2ref = {}
51
+ @schema_names = Set.new
52
+ @items = {}
53
+ @ignored_keys = Set.new(ignored_keys)
54
+ end
55
+
56
+ def add_options(opts)
57
+ opts.on('--use FIELD', 'Use FIELD in comparisons.') do |f|
58
+ @ignored_keys.delete(f)
59
+ end
60
+ opts.on('--ignore FIELD', 'Ignore FIELD in comparisons.') do |f|
61
+ @ignored_keys.add(f)
62
+ end
63
+ end
64
+
65
+ def help
66
+ %(All fields are used in object equality comparisons except:\n#{@ignored_keys.to_a.sort!.join("\n")})
67
+ end
68
+
69
+ def add_schema_name(name)
70
+ @schema_names.add(name)
71
+ end
72
+
73
+ def ref_string(name)
74
+ return nil if name.nil?
75
+ "#{@path}#{name}"
76
+ end
77
+
78
+ def reference(obj)
79
+ # Check if identical schema has been added. If so, return the $ref string.
80
+ @items.each do |k, v|
81
+ return ref_string(k) if ApiObjects.same(obj, v, @ignored_keys)
82
+ end
83
+ # One of the numbers will not match existing keys. More number than keys.
84
+ (@items.size + 1).times do |n|
85
+ # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
86
+ cand = "#{@prefix}#{n}x"
87
+ next if @items.key?(cand)
88
+ @items[cand] = obj.merge
89
+ @schema_names.add(cand)
90
+ return ref_string(cand)
91
+ end
92
+ end
93
+
94
+ def store_anchor(obj, ref = nil)
95
+ anchor_name = obj['$anchor']
96
+ return if anchor_name.nil?
97
+ ref = obj['$ref'] if ref.nil?
98
+ raise StandardError, 'ref is nil and no $ref or it is nil' if ref.nil?
99
+ @anchor2ref[anchor_name] = ref
100
+ end
101
+
102
+ def alter_anchors
103
+ replacements = {}
104
+ @anchor2ref.each_key do |a|
105
+ next if @schema_names.member?(a)
106
+ replacements[a] = ref_string(a)
107
+ @schema_names.add(a)
108
+ end
109
+ replacements.each do |a, r|
110
+ @anchor2ref[a] = r
111
+ end
112
+ end
113
+
114
+ def anchor_ref_replacement(ref)
115
+ @anchor2ref[ref[1...ref.size]] || ref
116
+ end
117
+ end
118
+
119
+ # Represents path with fixed parts and variables.
120
+ class ServerPath
121
+ # Probably moves to a separate file once processpaths and frequencies receive
122
+ # some attention.
123
+ include Comparable
124
+
125
+ attr_accessor :parts
126
+
127
+ def initialize(parts)
128
+ @parts = parts
129
+ end
130
+
131
+ # Parameters are after fixed strings.
132
+ def <=>(other)
133
+ pp = other.is_a?(Array) ? other : other.parts
134
+ @parts.size.times do |k|
135
+ return 1 if pp.size <= k # Longer comes after shorter.
136
+ pk = @parts[k]
137
+ ppk = pp[k]
138
+ if pk.key?('fixed')
139
+ if ppk.key?('fixed')
140
+ c = pk['fixed'] <=> ppk['fixed']
141
+ else
142
+ return -1
143
+ end
144
+ else
145
+ if ppk.key?('fixed')
146
+ return 1
147
+ else
148
+ c = pk.fetch('parameter', '') <=> ppk.fetch('parameter', '')
149
+ end
150
+ end
151
+ return c unless c.zero?
152
+ end
153
+ (@parts.size < pp.size) ? -1 : 0
154
+ end
155
+
156
+ # Not fit for sorting. Variable equals anything.
157
+ def compare(other, range = nil)
158
+ pp = other.is_a?(Array) ? other : other.parts
159
+ if range.nil?
160
+ range = 0...@parts.size
161
+ elsif range.is_a? Number
162
+ range = range...(range + 1)
163
+ end
164
+ range.each do |k|
165
+ return 1 if pp.size <= k # Longer comes after shorter.
166
+ ppk = pp[k]
167
+ next unless ppk.key?('fixed')
168
+ pk = parts[k]
169
+ next unless pk.key?('fixed')
170
+ c = pk['fixed'] <=> ppk['fixed']
171
+ return c unless c.zero?
172
+ end
173
+ (@parts.size < pp.size) ? -1 : 0
174
+ end
175
+ end
176
+
177
+ def self.operation_objects(path_item)
178
+ keys = %w[operationId requestBody responses callbacks]
179
+ out = {}
180
+ path_item.each do |method, op|
181
+ next unless op.is_a?(Hash)
182
+ keys.each do |key|
183
+ next unless op.key?(key)
184
+ out[method] = op
185
+ break
186
+ end
187
+ end
188
+ out
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2021-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require 'pathname'
7
+ require 'yaml'
8
+
9
+ module OpenAPISourceTools
10
+ # Common methods used in programs and elsewhere gathered into one place.
11
+ module Common
12
+ def self.aargh(message, return_value = nil)
13
+ message = message.map(&:to_s).join("\n") if message.is_a? Array
14
+ $stderr.puts message
15
+ return_value
16
+ end
17
+
18
+ def self.yesno(boolean)
19
+ boolean ? 'yes' : 'no'
20
+ end
21
+
22
+ def self.bury(doc, path, value)
23
+ (path.size - 1).times do |k|
24
+ p = path[k]
25
+ doc[p] = {} unless doc.key?(p)
26
+ doc = doc[p]
27
+ end
28
+ doc[path.last] = value
29
+ end
30
+
31
+ module Out
32
+ attr_reader :count
33
+ module_function :count
34
+ attr_accessor :quiet
35
+ module_function :quiet
36
+ module_function :quiet=
37
+
38
+ def self.put(message)
39
+ Common.aargh(message) unless @quiet
40
+ @count = @count.nil? ? 1 : @count + 1
41
+ end
42
+ end
43
+
44
+ def self.split_path(p, spec = false)
45
+ parts = []
46
+ p = p.strip
47
+ unless spec
48
+ q = p.index('?')
49
+ p.slice!(0...q) unless q.nil?
50
+ end
51
+ p.split('/').each do |s|
52
+ next if s.empty?
53
+ s = { (spec && s.include?('{') ? 'parameter' : 'fixed') => s }
54
+ parts.push(s)
55
+ end
56
+ parts
57
+ end
58
+
59
+ def self.load_source(input)
60
+ YAML.safe_load(input.nil? ? $stdin : File.read(input))
61
+ rescue Errno::ENOENT
62
+ aargh "Could not load #{input || 'stdin'}"
63
+ rescue StandardError => e
64
+ aargh "#{e}\nFailed to read #{input || 'stdin'}"
65
+ end
66
+
67
+ def self.dump_result(output, doc, error_return)
68
+ doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
69
+ if output.nil?
70
+ $stdout.puts doc
71
+ else
72
+ fp = Pathname.new output
73
+ fp.open('w') do |f|
74
+ f.puts doc
75
+ end
76
+ end
77
+ 0
78
+ rescue StandardError => e
79
+ aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,158 @@
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
+ require_relative 'gen'
8
+ require 'find'
9
+ require 'yaml'
10
+
11
+ module OpenAPISourceTools
12
+ # Configuration file find and load convenience functions.
13
+ # See the first 3 methods. The rest are intended to be internal helpers.
14
+ module ConfigLoader
15
+
16
+ # A function to find all files with a given prefix.
17
+ # Prefix is taken from Gen.config if nil.
18
+ # Returns an array of ConfigFileInfo objects.
19
+ def self.find_files(name_prefix:, extensions: [ '.*' ])
20
+ name_prefix = Gen.config if name_prefix.nil?
21
+ raise ArgumentError, 'name_prefix or config must be set' if name_prefix.nil?
22
+ root, name_prefix = prepare_prefix(name_prefix, Gen.wd)
23
+ file_paths = find_filenames(root, name_prefix)
24
+ splitter = path_splitter(Gen.separator)
25
+ out = file_paths.map { |fp| convert_path_end(fp, splitter, root.size + 1, extensions) }
26
+ out.sort!
27
+ end
28
+
29
+ # A function to read all YAML files in an array of ConfigFileInfo objects.
30
+ # Returns the same as contents_array.
31
+ def self.read_contents(config_file_infos)
32
+ config_file_infos.each do |cfi|
33
+ c = YAML.safe_load_file(cfi.path)
34
+ # Check allows e.g. copyright and license files be named with config prefix
35
+ # for clarity, but ignored during config loading.
36
+ next if cfi.keys.empty? && !c.is_a?(Hash)
37
+ cfi.bury_content(c)
38
+ rescue Psych::SyntaxError
39
+ next # Was not YAML. Other files can be named using config prefix.
40
+ end
41
+ contents_array(config_file_infos)
42
+ end
43
+
44
+ # Maps an array of ConfigFileInfo objects to an array of their contents.
45
+ def self.contents_array(config_file_infos)
46
+ config_file_infos.map(&:content).reject(&:nil?)
47
+ end
48
+
49
+ class ConfigFileInfo
50
+ include Comparable
51
+
52
+ attr_reader :root, :keys, :path, :content
53
+
54
+ def initialize(pieces, path)
55
+ @keys = []
56
+ @root = nil
57
+ pieces.each do |p|
58
+ if p.is_a?(String)
59
+ if @root.nil?
60
+ @root = p
61
+ else
62
+ @keys.push(p)
63
+ end
64
+ else
65
+ break if p == :extension
66
+ end
67
+ end
68
+ @path = path
69
+ @content = nil
70
+ end
71
+
72
+ def bury_content(content)
73
+ # Turns chain of keys into nested Hashes.
74
+ @keys.reverse.each do |key|
75
+ c = { key => content }
76
+ content = c
77
+ end
78
+ @content = content
79
+ end
80
+
81
+ def <=>(other)
82
+ d = @root <=> other.root
83
+ return d unless d.zero?
84
+ d = @keys.size <=> other.keys.size
85
+ return d unless d.zero?
86
+ d = @keys <=> other.keys
87
+ return d unless d.zero?
88
+ @path <=> other.path
89
+ end
90
+ end
91
+
92
+ def self.prepare_prefix(name_prefix, root)
93
+ name_prefix_dir = File.dirname(name_prefix)
94
+ root = File.realpath(name_prefix_dir, root) unless name_prefix_dir == '.'
95
+ name_prefix = File.basename(name_prefix)
96
+ if name_prefix == '.' # Just being nice. Daft argument.
97
+ name_prefix = File.basename(root)
98
+ root = File.dirname(root)
99
+ end
100
+ [root, name_prefix]
101
+ end
102
+
103
+ def self.find_filenames(root, name_prefix)
104
+ full_prefix = File.join(root, name_prefix)
105
+ file_paths = []
106
+ Find.find(root) do |path|
107
+ next if path.size < full_prefix.size
108
+ is_dir = File.directory?(path)
109
+ if path.start_with?(full_prefix)
110
+ file_paths.push(path) unless is_dir
111
+ elsif is_dir
112
+ Find.prune
113
+ end
114
+ end
115
+ file_paths
116
+ end
117
+
118
+ def self.path_splitter(separator)
119
+ parts = [ Regexp.quote('/') ]
120
+ parts.push(Regexp.quote(separator)) if separator.is_a?(String) && !separator.empty?
121
+ Regexp.new("(#{parts.join('|')})")
122
+ end
123
+
124
+ def self.remove_extension(file_path, extensions)
125
+ extensions.each do |e|
126
+ if e == '.*'
127
+ idx = file_path.rindex('.')
128
+ next if idx.nil? # No . anywhere.
129
+ ext = file_path[idx..]
130
+ next unless ext.index('/').nil? # Last . is before file name.
131
+ return [ file_path[0...idx], ext ]
132
+ elsif file_path.end_with?(e)
133
+ return [ file_path[0..-(1 + e.size)], e ]
134
+ end
135
+ end
136
+ [ file_path, nil ]
137
+ end
138
+
139
+ def self.convert_path_end(path, splitter, prefix_size, extensions)
140
+ relevant, ext = remove_extension(path[prefix_size..], extensions)
141
+ pieces = relevant.split(splitter).map do |piece|
142
+ case piece
143
+ when '' then nil
144
+ when '/' then :dir
145
+ when Gen.separator then :separator
146
+ else
147
+ piece
148
+ end
149
+ end
150
+ unless ext.nil?
151
+ pieces.push(:extension)
152
+ pieces.push(ext)
153
+ end
154
+ pieces.compact!
155
+ ConfigFileInfo.new(pieces, path)
156
+ end
157
+ end
158
+ end