ellipses 0.0.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/BEN/304/260OKU.md +137 -0
  3. data/CHANGELOG.md +9 -0
  4. data/LICENSE.md +675 -0
  5. data/README.md +1 -0
  6. data/bin/src +12 -0
  7. data/bin/srv +12 -0
  8. data/ellipses.gemspec +39 -0
  9. data/lib/ellipses-client.rb +4 -0
  10. data/lib/ellipses-server.rb +4 -0
  11. data/lib/ellipses.rb +5 -0
  12. data/lib/ellipses/client.rb +17 -0
  13. data/lib/ellipses/client/application.rb +77 -0
  14. data/lib/ellipses/client/cli.rb +24 -0
  15. data/lib/ellipses/client/cli/compile.rb +21 -0
  16. data/lib/ellipses/client/cli/decompile.rb +21 -0
  17. data/lib/ellipses/client/cli/init.rb +23 -0
  18. data/lib/ellipses/client/cli/update.rb +19 -0
  19. data/lib/ellipses/client/cli/validate.rb +21 -0
  20. data/lib/ellipses/client/cli/version.rb +17 -0
  21. data/lib/ellipses/client/command.rb +69 -0
  22. data/lib/ellipses/client/commands.rb +23 -0
  23. data/lib/ellipses/client/commands/include.rb +22 -0
  24. data/lib/ellipses/client/commands/reject.rb +17 -0
  25. data/lib/ellipses/client/commands/select.rb +17 -0
  26. data/lib/ellipses/client/commands/substitute.rb +23 -0
  27. data/lib/ellipses/client/commands/translate.rb +15 -0
  28. data/lib/ellipses/client/config.rb +33 -0
  29. data/lib/ellipses/client/directive.rb +39 -0
  30. data/lib/ellipses/client/error.rb +10 -0
  31. data/lib/ellipses/client/lines.rb +154 -0
  32. data/lib/ellipses/client/meta.rb +79 -0
  33. data/lib/ellipses/client/meta_file.rb +56 -0
  34. data/lib/ellipses/client/parser.rb +74 -0
  35. data/lib/ellipses/client/repository.rb +101 -0
  36. data/lib/ellipses/client/source.rb +69 -0
  37. data/lib/ellipses/error.rb +5 -0
  38. data/lib/ellipses/server.rb +10 -0
  39. data/lib/ellipses/server/application.rb +85 -0
  40. data/lib/ellipses/server/cli.rb +21 -0
  41. data/lib/ellipses/server/cli/dump.rb +20 -0
  42. data/lib/ellipses/server/cli/validate.rb +21 -0
  43. data/lib/ellipses/server/cli/version.rb +17 -0
  44. data/lib/ellipses/server/error.rb +7 -0
  45. data/lib/ellipses/server/meta.rb +69 -0
  46. data/lib/ellipses/server/meta_file.rb +39 -0
  47. data/lib/ellipses/server/repository.rb +47 -0
  48. data/lib/ellipses/server/symbols.rb +146 -0
  49. data/lib/ellipses/support.rb +3 -0
  50. data/lib/ellipses/support/deflate_path.rb +14 -0
  51. data/lib/ellipses/support/digest.rb +13 -0
  52. data/lib/ellipses/support/entropy.rb +17 -0
  53. data/lib/ellipses/support/expand_path.rb +13 -0
  54. data/lib/ellipses/support/intersperse_arrays.rb +14 -0
  55. data/lib/ellipses/support/prefixize_non_blank.rb +16 -0
  56. data/lib/ellipses/support/refinements.rb +20 -0
  57. data/lib/ellipses/support/sanitize_path.rb +101 -0
  58. data/lib/ellipses/support/search_path.rb +19 -0
  59. data/lib/ellipses/support/shell.rb +85 -0
  60. data/lib/ellipses/support/to_range.rb +15 -0
  61. data/lib/ellipses/support/ui.rb +55 -0
  62. data/lib/ellipses/support/updatelines.rb +19 -0
  63. data/lib/ellipses/support/writelines.rb +11 -0
  64. data/lib/ellipses/version.rb +5 -0
  65. metadata +264 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Ellipses
6
+ module Server
7
+ class Meta
8
+ Error = Class.new Error
9
+
10
+ using Support::Refinements::Struct::FromHashWithoutBogusKeys
11
+
12
+ Global = Struct.new(:depends, :extension, :root, keyword_init: true) do
13
+ def self.from_hash(hash)
14
+ from_hash_without_bogus_keys!(hash, error: Error)
15
+ end
16
+ end
17
+
18
+ Symbol = Struct.new(:symbol, :description, :path, :depends, keyword_init: true) do
19
+ def to_s
20
+ symbol
21
+ end
22
+
23
+ def add_depends(*depends)
24
+ return if depends&.empty?
25
+
26
+ self.depends ||= []
27
+ depends.reverse_each { |member| self.depends.unshift(member) }
28
+ end
29
+
30
+ def self.from_hash(hash)
31
+ from_hash_without_bogus_keys!(hash, error: Error)
32
+ end
33
+
34
+ def self.from_hash_with_depends(hash, *depends)
35
+ from_hash_without_bogus_keys!(hash, error: Error).tap { |symbol| symbol.add_depends(*depends) }
36
+ end
37
+ end
38
+
39
+ class Symbols
40
+ extend Forwardable
41
+
42
+ def_delegators :@symbols, :each, :length
43
+
44
+ attr_reader :symbols
45
+
46
+ def initialize(symbols, *depends)
47
+ hash = {}
48
+
49
+ (symbols || []).each do |meta|
50
+ symbol = Symbol.from_hash_with_depends(meta, *depends)
51
+ hash[symbol.to_s] = symbol
52
+ end
53
+
54
+ @symbols = hash.values
55
+ end
56
+ end
57
+
58
+ attr_reader :symbols, :global
59
+
60
+ def initialize(hash)
61
+ symbols_array = hash.delete('symbols') || []
62
+ global_hash = hash || {}
63
+
64
+ @global = Global.from_hash(global_hash)
65
+ @symbols = Symbols.new(symbols_array, *(@global.depends || []))
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tomlrb'
4
+
5
+ module Ellipses
6
+ module Server
7
+ class MetaFile
8
+ FILE = 'src.toml'
9
+
10
+ Error = Class.new(Error)
11
+
12
+ attr_reader :directory
13
+
14
+ def initialize(directory)
15
+ @directory = Support.dir!(directory, error: Error)
16
+ @file = Support.file!(FILE, base: @directory, error: Error)
17
+ end
18
+
19
+ def to_s
20
+ @file
21
+ end
22
+
23
+ def read
24
+ Meta.new Tomlrb.load_file(@file)
25
+ rescue StandardError => e
26
+ raise Error, "Error while loading meta file: #{file}: #{e.message}"
27
+ end
28
+
29
+ class << self
30
+ def valid?(directory)
31
+ new(directory)
32
+ true
33
+ rescue Error
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Ellipses
6
+ module Server
7
+ class Repository
8
+ Error = Class.new Error
9
+
10
+ attr_reader :symbols, :global, :root
11
+
12
+ def initialize(symbols:, global:, directory:)
13
+ @symbols = symbols
14
+ @global = global
15
+ @root = global.root ? ::File.join(directory, global.root) : directory
16
+ @consumed = Set.new
17
+ end
18
+
19
+ def [](string)
20
+ (chunks = []).tap do
21
+ symbols.resolve(string).each { |symbol| yield_symbol(chunks, symbol) }
22
+ next unless chunks.empty?
23
+
24
+ raise Error, "No chunks resolved for symbol: #{string}, which may already have been consumed"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def yield_symbol(chunks, symbol)
31
+ return if @consumed.include? symbol
32
+
33
+ @consumed << symbol
34
+ return if (lines = symbol.payload(root, global.extension)).empty?
35
+
36
+ chunks << lines
37
+ end
38
+
39
+ class << self
40
+ def load(directory)
41
+ meta = (meta_file = MetaFile.new(directory)).read
42
+ new symbols: Symbols.new(meta.symbols), global: meta.global, directory: meta_file.directory
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Ellipses
6
+ module Server
7
+ class Symbols
8
+ CircularReferenceError = Class.new Error
9
+ MissingSymbolError = Class.new Error
10
+ BogusLeafError = Class.new Error
11
+ EmptyPayloadError = Class.new Error
12
+
13
+ class Symbol
14
+ attr_reader :depends
15
+
16
+ def initialize(meta)
17
+ @meta = meta
18
+ @depends = []
19
+ end
20
+
21
+ def build(symbols)
22
+ return if @meta.depends.nil?
23
+
24
+ # To avoid a circular reference, skip dependency if it is this symbol
25
+ @meta.depends.uniq.reject { |depend| depend == to_s }.each { |depend| depends << symbols.register(depend) }
26
+ end
27
+
28
+ def to_s
29
+ @meta.to_s
30
+ end
31
+
32
+ def leaf?
33
+ depends.empty?
34
+ end
35
+
36
+ def path
37
+ @path ||= @meta.path
38
+ end
39
+
40
+ def payload(rootdir, extension = nil)
41
+ unless (file = where(rootdir, extension)) && !::File.directory?(file)
42
+ raise BogusLeafError, "No source found for leaf symbol: #{self}" if leaf?
43
+
44
+ return []
45
+ end
46
+
47
+ ::File.readlines(file).tap do |lines|
48
+ raise EmptyPayloadError, "Empty source for symbol: #{self}" if lines.empty?
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def where(rootdir, extension = nil)
55
+ return ::File.exist?(path) ? ::File.join(rootdir, path) : nil if path
56
+
57
+ paths = [base = ::File.join(rootdir, to_s)]
58
+ paths.prepend("#{base}#{extension}") if extension
59
+
60
+ paths.find { |path| ::File.exist?(path) }
61
+ end
62
+
63
+ class << self
64
+ def from_string(string)
65
+ new Meta::Symbol.from_hash symbol: string
66
+ end
67
+
68
+ def from_meta(meta)
69
+ new meta
70
+ end
71
+ end
72
+
73
+ private_class_method :new
74
+ end
75
+
76
+ private_constant :Symbol
77
+
78
+ def initialize(meta)
79
+ @meta = meta
80
+ @registry = {}
81
+
82
+ build
83
+ end
84
+
85
+ def [](string)
86
+ raise MissingSymbolError, "Missing symbol: #{string}" unless @registry.key? string
87
+
88
+ @registry[string]
89
+ end
90
+
91
+ def resolve(string)
92
+ resolved = []
93
+ walk(self[string]) { |symbol| resolved << symbol }
94
+ resolved
95
+ end
96
+
97
+ def register(string)
98
+ _register Symbol.from_string(string)
99
+ end
100
+
101
+ def walk(symbol, &block)
102
+ _walk(symbol, Set[symbol], &block)
103
+ end
104
+
105
+ private
106
+
107
+ def build
108
+ @meta.each { |symbol_meta| _register_by_meta(symbol_meta) }
109
+ @registry.dup.each_value { |symbol| symbol.build(self) }
110
+
111
+ sanitize
112
+ end
113
+
114
+ def _walk(symbol, stack, &block)
115
+ return if symbol.depends.nil?
116
+
117
+ symbol.depends.each do |depend|
118
+ raise CircularReferenceError, "Circular reference from #{symbol} to #{depend}" if stack.first == depend
119
+
120
+ next if stack.include? depend
121
+
122
+ stack << depend
123
+ _walk(depend, stack, &block)
124
+ rescue ::StopIteration
125
+ return nil
126
+ end
127
+
128
+ yield(symbol) if block
129
+ end
130
+
131
+ def _register(symbol)
132
+ @registry[symbol.to_s] || (@registry[symbol.to_s] = symbol)
133
+ end
134
+
135
+ def _register_by_meta(symbol_meta)
136
+ _register Symbol.from_meta(symbol_meta)
137
+ end
138
+
139
+ def sanitize
140
+ tap do
141
+ @registry.each_value { |symbol| walk(symbol) }
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, 'support', '*.rb')].each { |support| require support }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Ellipses
6
+ module Support
7
+ module_function
8
+
9
+ def deflate_path(path, rootdir = nil)
10
+ base = Pathname.new(rootdir || '.').expand_path
11
+ Pathname.new(path).cleanpath.expand_path.relative_path_from(base).to_s
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Ellipses
6
+ module Support
7
+ module_function
8
+
9
+ def digest(*args)
10
+ ::Digest::SHA256.hexdigest args.map(&:to_s).join
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ellipses
4
+ module Support
5
+ module_function
6
+
7
+ # https://rosettacode.org/wiki/Entropy#Ruby
8
+ def entropy(string)
9
+ string
10
+ .each_char
11
+ .group_by(&:to_s)
12
+ .values
13
+ .map { |x| x.length / string.length.to_f }
14
+ .reduce(0) { |e, x| e - x * Math.log2(x) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Ellipses
6
+ module Support
7
+ module_function
8
+
9
+ def expand_path(path, rootdir = nil)
10
+ Pathname.new(::File.join(rootdir || '.', path)).cleanpath.to_s
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ellipses
4
+ module Support
5
+ module_function
6
+
7
+ def intersperse_arrays(arrays, intersperse)
8
+ [].tap do |interspersed|
9
+ arrays.each { |array| interspersed.append(array, intersperse) }
10
+ interspersed.pop
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ellipses
4
+ module Support
5
+ module_function
6
+
7
+ def prefixize_non_blank(string, prefix, excludes: nil)
8
+ stripped = string.strip
9
+
10
+ return string if prefix.empty? || stripped.empty?
11
+ return string if excludes && [*excludes].include?(stripped)
12
+
13
+ "#{prefix}#{string}"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ellipses
4
+ module Support
5
+ module Refinements
6
+ module Struct
7
+ module FromHashWithoutBogusKeys
8
+ refine ::Struct.singleton_class do
9
+ def from_hash_without_bogus_keys!(hash, error:)
10
+ bogus = (hash = hash.transform_keys(&:to_sym)).keys.reject { |key| members.include?(key) }
11
+ raise error, "Bogus keys found: #{bogus}" unless bogus.empty?
12
+
13
+ new hash.slice(*members)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ellipses
4
+ module Support
5
+ module SanitizePath
6
+ class Sanitizer
7
+ def self.sanitize(*args, **kwargs)
8
+ new(*args, **kwargs).sanitize
9
+ end
10
+
11
+ class Error < StandardError
12
+ def initialize(path)
13
+ super("#{kind}: #{path}")
14
+ end
15
+
16
+ def self.kind(message)
17
+ Class.new self do
18
+ define_method(:kind) { message }
19
+ end
20
+ end
21
+ end
22
+
23
+ MissingBase = Error.kind 'Directory not found'
24
+ WrongBase = Error.kind 'Not a directory'
25
+ MissingPath = Error.kind 'File or directory not found'
26
+ NotFile = Error.kind 'Not a file'
27
+ NotDir = Error.kind 'Not a directory'
28
+ NotExecutable = Error.kind 'Not an executable'
29
+
30
+ attr_reader :path, :base, :fullpath
31
+
32
+ def initialize(path:, base: nil)
33
+ @path = ::File.join(*path)
34
+ @base = ::File.join(*base) if base
35
+ @fullpath = ::File.expand_path(@path, @base)
36
+ end
37
+
38
+ def sanitize
39
+ if base
40
+ raise MissingBase, base unless ::File.exist?(base)
41
+ raise WrongBase, base unless ::File.directory?(base)
42
+ end
43
+
44
+ raise MissingPath, fullpath unless ::File.exist?(fullpath)
45
+
46
+ call
47
+
48
+ fullpath
49
+ end
50
+
51
+ class Dir < Sanitizer
52
+ def call
53
+ raise NotDir, fullpath unless ::File.directory?(fullpath)
54
+ end
55
+ end
56
+
57
+ private_constant :Dir
58
+
59
+ class File < Sanitizer
60
+ def call
61
+ raise NotFile, fullpath unless ::File.file?(fullpath)
62
+ end
63
+ end
64
+
65
+ private_constant :File
66
+
67
+ class Executable < Sanitizer
68
+ def call
69
+ raise NotExecutable, fullpath unless ::File.file?(fullpath) || ::File.executable?(fullpath)
70
+ end
71
+ end
72
+
73
+ private_constant :Executable
74
+ end
75
+
76
+ private_constant :Sanitizer
77
+
78
+ def self.extended(consumer) # rubocop:disable Metrics/MethodLength
79
+ super
80
+
81
+ %i[file dir executable].each do |meth|
82
+ klass = Sanitizer.const_get meth.capitalize
83
+
84
+ consumer.define_singleton_method(meth) do |path, base: nil|
85
+ klass.sanitize(path: path, base: base)
86
+ rescue Sanitizer::Error
87
+ nil
88
+ end
89
+
90
+ consumer.define_singleton_method("#{meth}!") do |path, error: nil, base: nil|
91
+ klass.sanitize(path: path, base: base)
92
+ rescue Sanitizer::Error => e
93
+ error ? raise(error, e.message) : raise
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ extend SanitizePath
100
+ end
101
+ end