glazed-oxidized 0.1.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 +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +202 -0
- data/README.md +16 -0
- data/Rakefile +12 -0
- data/lib/glazed/oxidized/bridge.rb +148 -0
- data/lib/glazed/oxidized/errors.rb +21 -0
- data/lib/glazed/oxidized/model_box.rb +115 -0
- data/lib/glazed/oxidized/model_dialect.rb +119 -0
- data/lib/glazed/oxidized/node_view.rb +69 -0
- data/lib/glazed/oxidized/prelude.rb +708 -0
- data/lib/glazed/oxidized/secret_substitution.rb +105 -0
- data/lib/glazed/oxidized/version.rb +11 -0
- data/lib/glazed/oxidized.rb +93 -0
- metadata +71 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed/must'
|
|
7
|
+
|
|
8
|
+
module Glazed
|
|
9
|
+
module Oxidized
|
|
10
|
+
# A substitution from a model's `cmd :secret` block, which hides secrets
|
|
11
|
+
# by replacing matches of a pattern. The secret is what the replacement
|
|
12
|
+
# drops from a match.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# substitution = Glazed::Oxidized::SecretSubstitution.new(pattern: /^(snmp-server community) \S+/, replacement: '\\1 <hidden>')
|
|
16
|
+
# match = substitution.pattern.match('snmp-server community public')
|
|
17
|
+
# substitution.offsets(match) #=> 22...28
|
|
18
|
+
class SecretSubstitution
|
|
19
|
+
# Backreferences in a replacement, as String#sub expands them.
|
|
20
|
+
BACKREFERENCE = /\\(?:(\d)|k<(\w+)>|([&`'+\\]))/ #: Regexp
|
|
21
|
+
|
|
22
|
+
# @param pattern [Regexp] pattern the block substitutes
|
|
23
|
+
# @param replacement [String] replacement, with backreferences
|
|
24
|
+
#: (pattern: Regexp, replacement: String) -> void
|
|
25
|
+
def initialize(pattern:, replacement:)
|
|
26
|
+
@pattern = pattern #: Regexp
|
|
27
|
+
@replacement = replacement #: String
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Regexp] pattern the block substitutes
|
|
31
|
+
#: Regexp
|
|
32
|
+
attr_reader :pattern
|
|
33
|
+
# @return [String] replacement, with backreferences
|
|
34
|
+
#: String
|
|
35
|
+
attr_reader :replacement
|
|
36
|
+
|
|
37
|
+
# Finds the text the replacement drops from a match: the match without
|
|
38
|
+
# the start and end it shares with its replacement, and without
|
|
39
|
+
# surrounding whitespace. A shared start or end never ends inside a word,
|
|
40
|
+
# so a secret beginning like the replacement is not partly kept.
|
|
41
|
+
#
|
|
42
|
+
# @param match [MatchData] a match of {pattern}
|
|
43
|
+
# @return [Range<Integer>, nil] offsets of the dropped text in the matched string, or nil when nothing is dropped
|
|
44
|
+
#: (MatchData match) -> Range[Integer]?
|
|
45
|
+
def offsets(match)
|
|
46
|
+
matched = match[0].to_s
|
|
47
|
+
replaced = expand(match)
|
|
48
|
+
head = shared_length(matched, replaced)
|
|
49
|
+
head -= 1 while head.positive? && word?(matched[head - 1]) && word?(matched[head])
|
|
50
|
+
tail = shared_length(matched[head..].to_s.reverse, replaced[head..].to_s.reverse)
|
|
51
|
+
tail -= 1 while tail.positive? && word?(matched[-tail - 1]) && word?(matched[-tail])
|
|
52
|
+
start = head
|
|
53
|
+
finish = matched.size - tail
|
|
54
|
+
start += 1 while start < finish && whitespace?(matched[start])
|
|
55
|
+
finish -= 1 while finish > start && whitespace?(matched[finish - 1])
|
|
56
|
+
return nil if start >= finish
|
|
57
|
+
|
|
58
|
+
origin = match.begin(0)
|
|
59
|
+
(origin + start)...(origin + finish)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param match [MatchData] a match of {pattern}
|
|
63
|
+
# @return [String] the replacement with its backreferences expanded
|
|
64
|
+
#: (MatchData match) -> String
|
|
65
|
+
private def expand(match)
|
|
66
|
+
replacement.gsub(BACKREFERENCE) do
|
|
67
|
+
reference = Must.expect(Regexp.last_match)
|
|
68
|
+
if (group = reference[1])
|
|
69
|
+
match[group.to_i].to_s
|
|
70
|
+
elsif (name = reference[2])
|
|
71
|
+
match[name].to_s
|
|
72
|
+
else
|
|
73
|
+
case reference[3]
|
|
74
|
+
when '&' then match[0].to_s
|
|
75
|
+
when '`' then match.pre_match
|
|
76
|
+
when "'" then match.post_match
|
|
77
|
+
when '+' then match.captures.compact.last.to_s
|
|
78
|
+
else '\\'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param left [String] a string
|
|
85
|
+
# @param right [String] another string
|
|
86
|
+
# @return [Integer] length of the start the strings share
|
|
87
|
+
#: (String left, String right) -> Integer
|
|
88
|
+
private def shared_length(left, right)
|
|
89
|
+
length = 0
|
|
90
|
+
length += 1 while length < left.size && length < right.size && left[length] == right[length]
|
|
91
|
+
length
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param char [String, nil] a character
|
|
95
|
+
# @return [Boolean] whether it is a word character
|
|
96
|
+
#: (String? char) -> bool
|
|
97
|
+
private def word?(char) = char&.match?(/\w/) || false
|
|
98
|
+
|
|
99
|
+
# @param char [String, nil] a character
|
|
100
|
+
# @return [Boolean] whether it is whitespace
|
|
101
|
+
#: (String? char) -> bool
|
|
102
|
+
private def whitespace?(char) = char&.match?(/\s/) || false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/dialects'
|
|
8
|
+
require 'glazed/must'
|
|
9
|
+
require 'glazed/oxidized/errors'
|
|
10
|
+
require 'glazed/oxidized/model_box'
|
|
11
|
+
require 'glazed/oxidized/model_dialect'
|
|
12
|
+
require 'glazed/oxidized/version'
|
|
13
|
+
|
|
14
|
+
module Glazed
|
|
15
|
+
# Runs Oxidized model files as glazed dialects. A node whose dialect is
|
|
16
|
+
# "oxidized_<model>" runs the model file `<model>.rb` found in {model_paths}.
|
|
17
|
+
#
|
|
18
|
+
# Each model is loaded into its own Ruby::Box, which needs RUBY_BOX=1 in the
|
|
19
|
+
# environment of the Ruby process.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# require 'glazed/oxidized'
|
|
23
|
+
# Glazed::Dialects.find('oxidized_aireos') #=> Glazed::Dialects::OxidizedAireos
|
|
24
|
+
module Oxidized
|
|
25
|
+
# Prefix of dialect names that run an Oxidized model.
|
|
26
|
+
DIALECT_PREFIX = 'oxidized_' #: String
|
|
27
|
+
|
|
28
|
+
@model_paths = nil #: Array[String]?
|
|
29
|
+
@lock = Mutex.new #: Thread::Mutex
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Directories searched for model files, in order. Defaults to Oxidized's
|
|
33
|
+
# own order: the model directory under $OXIDIZED_HOME (or
|
|
34
|
+
# ~/.config/oxidized), then the models of the installed oxidized gem.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String>] directories, which callers may change
|
|
37
|
+
#: () -> Array[String]
|
|
38
|
+
def model_paths
|
|
39
|
+
@model_paths ||= default_model_paths
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param paths [Array<String>] directories searched for model files
|
|
43
|
+
# @return [void]
|
|
44
|
+
#: (Array[String] paths) -> void
|
|
45
|
+
def model_paths=(paths)
|
|
46
|
+
@model_paths = paths
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param name [String] model name, such as "ios"
|
|
50
|
+
# @return [String, nil] absolute path of the first model file by that name in {model_paths}
|
|
51
|
+
#: (String name) -> String?
|
|
52
|
+
def model_path(name)
|
|
53
|
+
return nil unless name.match?(/\A[a-z0-9_]+\z/)
|
|
54
|
+
|
|
55
|
+
model_paths.map { File.join(File.expand_path(it), "#{name}.rb") }.find { File.file?(it) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Builds the dialect running a model and registers it under
|
|
59
|
+
# {Dialects}, so later lookups find it without loading the model again.
|
|
60
|
+
#
|
|
61
|
+
# @param name [String] model name, such as "ios"
|
|
62
|
+
# @return [Class<ModelDialect>, nil] the dialect, or nil when no model file has the name
|
|
63
|
+
# @raise [UnavailableError] when Ruby::Box is not enabled
|
|
64
|
+
# @raise [ModelNotFound] when the file defines no model class by its name
|
|
65
|
+
#: (String name) -> singleton(ModelDialect)?
|
|
66
|
+
def dialect(name)
|
|
67
|
+
constant_name = "#{DIALECT_PREFIX}#{name}".split('_').map(&:capitalize).join
|
|
68
|
+
@lock.synchronize do
|
|
69
|
+
if Dialects.const_defined?(constant_name, false)
|
|
70
|
+
return Must.be_a(Dialects.const_get(constant_name, false), Class) #: as singleton(ModelDialect)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
path = model_path(name) or return nil
|
|
74
|
+
dialect = ModelDialect.build(ModelBox.new(name:, path:))
|
|
75
|
+
Dialects.const_set(constant_name, dialect)
|
|
76
|
+
dialect
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Array<String>] Oxidized's model directories
|
|
81
|
+
#: () -> Array[String]
|
|
82
|
+
private def default_model_paths
|
|
83
|
+
home = ENV.fetch('OXIDIZED_HOME') { File.join(Dir.home, '.config', 'oxidized') }
|
|
84
|
+
paths = [File.join(home, 'model')]
|
|
85
|
+
gem = Gem::Specification.find_all_by_name('oxidized').max_by(&:version)
|
|
86
|
+
paths << File.join(gem.gem_dir, 'lib', 'oxidized', 'model') if gem
|
|
87
|
+
paths
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Dialects.add_resolver { |name| dialect(name.delete_prefix(DIALECT_PREFIX)) if name.start_with?(DIALECT_PREFIX) }
|
|
92
|
+
end
|
|
93
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: glazed-oxidized
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sorah Fukumori
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: glazed
|
|
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'
|
|
26
|
+
email:
|
|
27
|
+
- her@sorah.jp
|
|
28
|
+
executables: []
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- ".yardopts"
|
|
33
|
+
- CHANGELOG.md
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- lib/glazed/oxidized.rb
|
|
38
|
+
- lib/glazed/oxidized/bridge.rb
|
|
39
|
+
- lib/glazed/oxidized/errors.rb
|
|
40
|
+
- lib/glazed/oxidized/model_box.rb
|
|
41
|
+
- lib/glazed/oxidized/model_dialect.rb
|
|
42
|
+
- lib/glazed/oxidized/node_view.rb
|
|
43
|
+
- lib/glazed/oxidized/prelude.rb
|
|
44
|
+
- lib/glazed/oxidized/secret_substitution.rb
|
|
45
|
+
- lib/glazed/oxidized/version.rb
|
|
46
|
+
homepage: https://github.com/sorah/glazed
|
|
47
|
+
licenses:
|
|
48
|
+
- Apache-2.0
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://github.com/sorah/glazed
|
|
51
|
+
source_code_uri: https://github.com/sorah/glazed/tree/main/glazed-oxidized
|
|
52
|
+
changelog_uri: https://github.com/sorah/glazed/blob/main/glazed-oxidized/CHANGELOG.md
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 4.0.0
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.20
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Oxidized model compatibility layer for glazed
|
|
71
|
+
test_files: []
|