ellipses 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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