trees 0.1.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc7886d327dd2a8f6d2a3833a08ba2b555b198d441c0348e276670514fde2b5f
4
- data.tar.gz: a7a6a657bb62fa25936a50b4c991e260e0d1daf6b421c063b4184a088f89b57a
3
+ metadata.gz: 1ac49d1c7aaa58a8ff86b29db7c2b7915c470094de6b3c1af4fbd4ad45adc597
4
+ data.tar.gz: 4a02b57c93cec8b8b42c36f39b158d0cca4c6d2a907b2c72bfdbfb38c5b57a74
5
5
  SHA512:
6
- metadata.gz: ddd1461e661cfc499e54d516f801ececa8fafbfd3613f9bbb119d2ee14b62701d6b3e05784f3539619cbcd75adbb3476209416e2e3fa64b2a09a42342f23e7c4
7
- data.tar.gz: af7bb3374418c5b39151076c07fceb1c38d58bf44deb890bfffb4d88c5a0a2218477e40e830e29418fecb374bc6804c3c52e722ed575260be99e31e2a0928674
6
+ metadata.gz: f37c39226cd76bfc232ef4933aa6221b8bf6069c90a0af11e2160dcbe62be6ba2072b63f7562c0b63e44f69eed949e380a59b58302cc52cbb2fa8d39e36429f7
7
+ data.tar.gz: 0b5c295977cb0b50c80c1c0fcbd271a5923f80943cc08efc0ded1ce333008723c21532c33c8e376adf6de5d718610b26b79f23d7fa3b0ee4b84d349fc0f0cd61
data/lib/line.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'low_type'
4
+
5
+ module Trees
6
+ class Line
7
+ include LowType
8
+
9
+ attr_reader :path, :params
10
+ attr_accessor :summary, :execute
11
+
12
+ def initialize(path: String, params: {})
13
+ @path = path
14
+ @params = params
15
+
16
+ @summary = nil
17
+ @execute = nil
18
+ end
19
+ end
20
+ end
data/lib/trees.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'line'
4
+ require_relative 'trie'
5
+
6
+ module Trees
7
+ attr_reader :lines, :trie
8
+
9
+ def line(path, &block)
10
+ current_path << path
11
+ @current_line = Line.new(path: current_path.join, params: block.parameters.to_h)
12
+
13
+ lines[current_path.join] = @current_line
14
+ trie.merge(line: @current_line)
15
+
16
+ block.call if block_given?
17
+
18
+ current_path.pop
19
+ end
20
+
21
+ def summary(&block)
22
+ @current_line.summary = block
23
+ end
24
+
25
+ def execute(&block)
26
+ @current_line.execute = block
27
+ end
28
+
29
+ def run(tokens)
30
+ results = trie.match(tokens:)
31
+
32
+ execute_block(results:)
33
+ end
34
+
35
+ private
36
+
37
+ def execute_block(results:)
38
+ while results.count > 0
39
+ result = results.pop
40
+
41
+ if result.line.execute
42
+ result.line.params.values.each do |param|
43
+ result.line.execute.binding.local_variable_set(param, result.params[param])
44
+ end
45
+
46
+ result.line.execute.call
47
+
48
+ return true
49
+ end
50
+ end
51
+
52
+ false
53
+ end
54
+
55
+ def current_path
56
+ @current_path ||= []
57
+ end
58
+
59
+ def lines
60
+ @lines ||= {}
61
+ end
62
+
63
+ def trie
64
+ @trie ||= Trie.new
65
+ end
66
+ end
data/lib/trie.rb ADDED
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'line'
4
+ require_relative 'trie_node'
5
+
6
+ module Trees
7
+ class Trie
8
+ include LowType
9
+
10
+ PARAM_DELIMITERS = [' ', ':'].freeze
11
+
12
+ attr_reader :root_node
13
+
14
+ Result = Data.define(:line, :params)
15
+
16
+ class DuplicatePathError < StandardError; end
17
+
18
+ def initialize
19
+ @root_node = TrieNode.new
20
+ end
21
+
22
+ def merge(line:, current_node: @root_node)
23
+ @current_index = 0
24
+ path = line.path
25
+
26
+ while @current_index < path.length
27
+ key = path[@current_index]
28
+
29
+ @current_index += 1
30
+
31
+ # Sometimes the key is an entire variable name.
32
+ key = capture_param(path:) if key == ':'
33
+
34
+ current_node = current_node.upsert_child(key:)
35
+ end
36
+
37
+ raise DuplicatePathError, "Path already defined: #{line.path}" if current_node.line
38
+
39
+ current_node.line = line
40
+ end
41
+
42
+ def match(tokens:, current_node: @root_node, index: 0, offset: 0, params: {})
43
+ return [] if tokens.empty?
44
+
45
+ [
46
+ *match_static(tokens:, current_node:, index:, offset:, params:),
47
+ *match_dynamic(tokens:, current_node:, index:, offset:, params:)
48
+ ]
49
+ end
50
+
51
+ private
52
+
53
+ # Match an input character with the trie or mimic the space character between tokens in the trie.
54
+ def match_static(tokens:, current_node:, index:, offset:, params:)
55
+ child_node = current_node.child(key: tokens[index][offset] || ' ')
56
+ return [] unless child_node
57
+
58
+ next_index, next_offset = advance(tokens:, index:, offset:)
59
+ result = end_match(child_node:, tokens:, index: next_index, offset: next_offset, params:)
60
+
61
+ [*result, *match(tokens:, current_node: child_node, index: next_index, offset: next_offset, params:)]
62
+ end
63
+
64
+ # Dynamic path segment: capture the remainder of the current token.
65
+ def match_dynamic(tokens:, current_node:, index:, offset:, params:)
66
+ current_node.params.flat_map do |param|
67
+ child_node = current_node.child(key: param)
68
+ arg, next_index, next_offset = capture_arg(tokens:, index:, offset:)
69
+ next_params = params.merge(param.delete_prefix(':').to_sym => arg)
70
+
71
+ result = end_match(child_node:, tokens:, index: next_index, offset: next_offset, params: next_params)
72
+
73
+ [*result, *match(tokens:, current_node: child_node, index: next_index, offset: next_offset, params: next_params)]
74
+ end
75
+ end
76
+
77
+ # Match an end node.
78
+ # TODO: Match a mid node.
79
+ def end_match(child_node:, tokens:, index:, offset:, params:)
80
+ if child_node.line && tokens[index + 1].nil?
81
+ return [Result.new(line: child_node.line, params:)]
82
+ end
83
+
84
+ []
85
+ end
86
+
87
+ def capture_param(path:)
88
+ param = [':']
89
+
90
+ path[@current_index...path.length].chars.each do |char|
91
+ break if PARAM_DELIMITERS.include?(char)
92
+
93
+ @current_index += 1
94
+ param << char
95
+ end
96
+
97
+ param.join
98
+ end
99
+
100
+ def advance(tokens:, index:, offset:)
101
+ token = tokens[index]
102
+
103
+ offset < token.length ? [index, offset + 1] : [index + 1, 0]
104
+ end
105
+
106
+ def capture_arg(tokens:, index:, offset:)
107
+ token = tokens[index]
108
+
109
+ [token[offset..], index, token.length]
110
+ end
111
+ end
112
+ end
data/lib/trie_node.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'line'
4
+
5
+ module Trees
6
+ class TrieNode
7
+ include LowType
8
+ using LowType::Syntax
9
+
10
+ # TODO: Represent empty hash/array return value as {}/[] rather than Hash/Array (https://github.com/low-rb/low_type/issues/16)
11
+ type_reader nodes: Hash[String => TrieNode] | Hash
12
+ type_reader params: Array[String] | Array
13
+ type_accessor line: Line | nil
14
+
15
+ def initialize
16
+ @nodes = {}
17
+ @params = []
18
+ @line = nil
19
+ end
20
+
21
+ def child(key: String)
22
+ @nodes[key]
23
+ end
24
+
25
+ def upsert_child(key: String)
26
+ @params << key if key.start_with?(':')
27
+ @nodes[key] || @nodes[key] = TrieNode.new
28
+ end
29
+ end
30
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trees
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trees
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: low_type
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  description: "A CLI framework where you write the literal terminal commands and \nTrees
13
27
  breaks them apart into a tree of options.\n"
14
28
  email:
@@ -17,7 +31,10 @@ executables: []
17
31
  extensions: []
18
32
  extra_rdoc_files: []
19
33
  files:
20
- - lib/plugs.rb
34
+ - lib/line.rb
35
+ - lib/trees.rb
36
+ - lib/trie.rb
37
+ - lib/trie_node.rb
21
38
  - lib/version.rb
22
39
  homepage: https://github.com/raindeer-rb/trees
23
40
  licenses: []
data/lib/plugs.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Trees
4
-
5
- end