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.
@@ -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,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ module Glazed
7
+ module Oxidized
8
+ # Gem version.
9
+ VERSION = '0.1.0' #: String
10
+ end
11
+ 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: []