ast-template 7.1.10 → 7.1.11

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: 4509a90704ed26fc5cf41a36d21cebde27bb6c0859831f61234b3e0f4236dc89
4
- data.tar.gz: 8764fbc1188e4d07232410d74bb073b45c6aa8f9f556c1ad3015621ff2d0bafd
3
+ metadata.gz: d333d88ffe0b32f1cf3bce7401deaaad28eaca1f40defae1d787e3fe782c6ebd
4
+ data.tar.gz: 903c10dc9ad0b5825ade7d2cd41fc0d9a2b0722b1ee0be425d41a5d70efe5c52
5
5
  SHA512:
6
- metadata.gz: be5a7b2e71c9b7132f6e9595e9795b0e8b2f614ae3a76ec9e9a386881a3af43d197e332ed39a9f07b5d1cf2eae25d6fe55c7ce8d055ca23a9dc449328b0e63fd
7
- data.tar.gz: a0048712a2b6158324b1991d026769dbd35ebc0a69820fec77c5987b69057eccda088d8bdcd0b266e49834c181e7449264f73c79990209b8a196bf68dd389b55
6
+ metadata.gz: 618776085a1f8680cba8e8640149129a71c8d9589a08c6a0536bef146a15d38db0fb5832b450d596d2368dc6a30c198be22f2e2b2c57a8e8ffa5a23343e8e991
7
+ data.tar.gz: f86daa1f153f431e3b27c8fd0fb9d63a4cf5f9af1657a6ea88116361e89db7de473e3e46d743d4bcd0e324be8a04b382977dcbe2b5816a9511402307b7ecdd84
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Template
5
+ # Destination-path file-type detection. Contract: structuredmerge/spec
6
+ # slice-1043, fixtures at
7
+ # structuredmerge/fixtures/templating/slice-1043-merge-dispatch.
8
+ #
9
+ # First match wins; the rule order is load-bearing (e.g. `.gemfile`
10
+ # shadows the appraisals rule for `Appraisal.root.gemfile` — preserved
11
+ # from kettle-jem's effective behavior and documented in the slice).
12
+ module FileTypes
13
+ RUBY_BASENAMES = %w[Gemfile Rakefile Appraisals Appraisal.root.gemfile .simplecov].freeze
14
+ RUBY_SUFFIXES = %w[.gemspec .gemfile].freeze
15
+ RUBY_EXTENSIONS = %w[.rb .rake].freeze
16
+
17
+ module_function
18
+
19
+ # @param path [String] logical destination path
20
+ # @param configured [String, Symbol, nil] destination-config override
21
+ # @return [Symbol] the file type
22
+ def detect(path, configured: nil)
23
+ return configured.to_sym unless configured.to_s.empty?
24
+
25
+ relative_path = path.to_s
26
+ basename = File.basename(relative_path)
27
+ extension = File.extname(relative_path).downcase
28
+ return :gitignore if basename == ".gitignore"
29
+ return :gemfile if basename == "Gemfile" || basename.end_with?(".gemfile")
30
+ return :appraisals if basename.start_with?("Appraisals")
31
+ return :gemspec if basename.end_with?(".gemspec")
32
+ return :rakefile if basename == "Rakefile" || extension == ".rake"
33
+ return :ruby if RUBY_BASENAMES.include?(basename) ||
34
+ RUBY_SUFFIXES.any? { |suffix| basename.end_with?(suffix) } ||
35
+ RUBY_EXTENSIONS.include?(extension)
36
+ return :yaml if extension.match?(/\A\.ya?ml\z/) || basename.casecmp("citation.cff").zero?
37
+ return :toml if extension == ".toml"
38
+ return :jsonc if relative_path == ".devcontainer/devcontainer.json"
39
+ return :jsonc if extension == ".jsonc"
40
+ return :json5 if extension == ".json5"
41
+ return :json if extension == ".json"
42
+ return :markdown if extension.match?(/\A\.md(?:own)?\z/)
43
+ return :dotenv if basename.start_with?(".env") || basename.end_with?(".env") || extension == ".env"
44
+ return :rbs if extension == ".rbs"
45
+
46
+ :text
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Template
5
+ # Merge dispatch: route a file type to its merger adapter. Contract:
6
+ # structuredmerge/spec slice-1043, fixtures at
7
+ # structuredmerge/fixtures/templating/slice-1043-merge-dispatch.
8
+ #
9
+ # Default adapters wrap the ast-merge family gems and are required LAZILY —
10
+ # ast-template does not hard-depend on merger gems; the consumer's bundle
11
+ # provides them. Types needing consumer policy (gemspec, the Ruby family,
12
+ # json variants, dotenv, rbs, bash, gitignore) have no default adapter —
13
+ # register one.
14
+ module MergeDispatch
15
+ Adapter = Struct.new(:gem_name, :require_path, :invoke)
16
+
17
+ DEFAULT_ADAPTERS = {
18
+ yaml: Adapter.new("psych-merge", "psych/merge", ->(t, d, **opts) { Psych::Merge.merge_yaml(t, d, "yaml", **opts) }),
19
+ toml: Adapter.new("citrus-toml-merge", "citrus/toml/merge", ->(t, d, **opts) { Citrus::Toml::Merge.merge_toml(t, d, "toml", **opts) }),
20
+ markdown: Adapter.new("markdown-merge", "markdown/merge", ->(t, d, **opts) { Markdown::Merge.merge_markdown(t, d, "markdown", **opts) })
21
+ }.freeze
22
+
23
+ @adapters = {}
24
+
25
+ class << self
26
+ # Register an adapter for a file type. The callable receives
27
+ # (template_content, destination_content, **options) and returns the
28
+ # merger result hash ({ok:, output:, diagnostics:}).
29
+ def register(file_type, callable = nil, &block)
30
+ adapter = callable || block
31
+ raise ArgumentError, "adapter callable required" unless adapter.respond_to?(:call)
32
+
33
+ adapters[file_type.to_sym] = adapter
34
+ end
35
+
36
+ # Whether any adapter (registered or default) exists for the type.
37
+ def registered?(file_type)
38
+ adapters.key?(file_type.to_sym) || DEFAULT_ADAPTERS.key?(file_type.to_sym)
39
+ end
40
+
41
+ # Dispatch to the type's adapter. Returns nil when no adapter exists —
42
+ # the caller's fallback is the template content.
43
+ #
44
+ # @return [Hash, nil] the merger result, or nil when unregistered
45
+ def dispatch(file_type, template_content, destination_content, **options)
46
+ type = file_type.to_sym
47
+ registered = adapters[type]
48
+ return registered.call(template_content, destination_content, **options) if registered
49
+
50
+ default = DEFAULT_ADAPTERS[type]
51
+ return nil unless default
52
+
53
+ begin
54
+ require default.require_path
55
+ rescue LoadError
56
+ raise LoadError, "merge dispatch for #{type} requires #{default.gem_name}; add it to the consumer bundle"
57
+ end
58
+ default.invoke.call(template_content, destination_content, **options)
59
+ end
60
+
61
+ # Test support: clear consumer-registered adapters.
62
+ def reset!
63
+ @adapters = {}
64
+ end
65
+
66
+ private
67
+
68
+ def adapters
69
+ @adapters ||= {}
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Template
5
+ # Strategy resolution and application decisions for template entries.
6
+ # Contract: structuredmerge/spec slice-1042, conformance fixtures at
7
+ # structuredmerge/fixtures/templating/slice-1042-strategy-application.
8
+ #
9
+ # The vocabulary (execution_action / write_action / status) matches the
10
+ # slice-353 session transport fixtures. The merger itself is the next
11
+ # contract surface (slice-1043); `merge` on differing content stops at
12
+ # the decision here.
13
+ module Strategies
14
+ # The four strategy names a destination config may declare.
15
+ STRATEGIES = %w[merge keep_destination raw_copy accept_template].freeze
16
+ DEFAULT_STRATEGY = "merge"
17
+
18
+ module_function
19
+
20
+ # Effective strategy for a destination path: the matching override wins,
21
+ # else the default. Overrides are TemplateStrategyOverride-shaped
22
+ # entries ({path:, strategy:} with string or symbol keys).
23
+ #
24
+ # @param default [String] strategy when no override matches
25
+ # @param overrides [Array<Hash>] per-path strategy overrides
26
+ # @param path [String] logical destination path
27
+ # @return [String] the effective strategy
28
+ def resolve(default: DEFAULT_STRATEGY, overrides: [], path:)
29
+ override = Array(overrides).find { |entry| (entry[:path] || entry["path"]) == path }
30
+ strategy = override && (override[:strategy] || override["strategy"])
31
+ strategy.to_s.empty? ? default : strategy.to_s
32
+ end
33
+
34
+ # The decision for one entry. Content is present only when this contract
35
+ # owns it (raw_copy, accept_template, merge-with-empty-destination);
36
+ # keep decisions and merge-of-differences return no content (the latter
37
+ # defers to the merger).
38
+ #
39
+ # @param strategy [String] resolved strategy for the entry
40
+ # @param template_content [String] prepared template content
41
+ # @param destination_content [String, nil] current destination content
42
+ # (nil when the destination file is absent)
43
+ # @return [Hash] {execution_action:, write_action:, status:, content:}
44
+ def plan_entry(strategy:, template_content:, destination_content:)
45
+ strategy = strategy.to_s
46
+ return keep_decision if strategy == "keep_destination"
47
+
48
+ absent = destination_content.nil?
49
+ if strategy == "merge"
50
+ trivial = trivial_merge_outcome(template_content, destination_content)
51
+ return keep_decision if trivial == :identical
52
+ return write_decision("merge_prepared_content", absent, template_content) if trivial == :empty_destination
53
+ end
54
+
55
+ # accept_template and merge-of-differences both write prepared content;
56
+ # for merge-of-differences the content is the merger's, not ours.
57
+ content = (strategy == "merge") ? nil : template_content
58
+ write_decision(execution_action_for(strategy), absent, content)
59
+ end
60
+
61
+ # Whether the strategy leaves the destination untouched. Absent
62
+ # destinations are not created under keep_destination.
63
+ def keep_destination?(strategy)
64
+ strategy.to_s == "keep_destination"
65
+ end
66
+
67
+ # The merge short-circuits that never invoke a merger:
68
+ # :empty_destination when there is nothing to merge into (template
69
+ # becomes the content), :identical when destination already equals
70
+ # template (destination is kept), nil when a real merge is required.
71
+ def trivial_merge_outcome(template_content, destination_content)
72
+ return :empty_destination if destination_content.to_s.strip.empty?
73
+ return :identical if destination_content == template_content
74
+
75
+ nil
76
+ end
77
+
78
+ def keep_decision
79
+ {execution_action: "keep", write_action: "keep", status: "keep", content: nil}
80
+ end
81
+
82
+ def write_decision(execution_action, create, content)
83
+ action = create ? "create" : "update"
84
+ {execution_action: execution_action, write_action: action, status: action, content: content}
85
+ end
86
+
87
+ def execution_action_for(strategy)
88
+ (strategy == "raw_copy") ? "raw_copy" : "merge_prepared_content"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'token/resolver'
4
+
5
+ module Ast
6
+ module Template
7
+ # Token application policy: normalize replacements, resolve with
8
+ # on-missing keep, then police tokens that survive in the consumer's
9
+ # namespace. Token mechanics (parsing, grammar) live in token-resolver;
10
+ # this module is the contract surface defined by
11
+ # structuredmerge/spec slice-1041 and its conformance fixtures.
12
+ module TokenApplication
13
+ # Raised when resolved output still contains tokens in the consumer's
14
+ # namespace. Carries the sorted, deduplicated canonical full keys so
15
+ # consumers can re-raise with their own branded message.
16
+ class UnresolvedTokensError < ArgumentError
17
+ # @return [Array<String>] canonical full token keys, sorted
18
+ attr_reader :tokens
19
+
20
+ def initialize(tokens)
21
+ @tokens = tokens
22
+ super("unresolved template tokens: #{tokens.map { |token| "{#{token}}" }.join(', ')}")
23
+ end
24
+ end
25
+
26
+ module_function
27
+
28
+ # Resolve tokens in content, then (when scanning) raise if any token in
29
+ # the consumer's namespace survived. Missing replacements are kept
30
+ # verbatim — in canonical form, so the scan sees canonical keys.
31
+ #
32
+ # @param content [String] template content
33
+ # @param replacements [Hash] token canonical key => value (keys and
34
+ # values are stringified before resolution)
35
+ # @param config [Token::Resolver::Config] token structure config
36
+ # @param namespace [String] first-segment namespace the consumer owns
37
+ # @param scan_unresolved [Boolean] when false, unresolved tokens pass
38
+ # @return [String] resolved content
39
+ # @raise [UnresolvedTokensError] unresolved namespaced tokens remain
40
+ def apply_tokens(content, replacements, config:, namespace:, scan_unresolved: true)
41
+ document = Token::Resolver::Document.new(content.to_s, config: config)
42
+ resolved = Token::Resolver::Resolve.new(on_missing: :keep).resolve(document, stringify_replacements(replacements))
43
+ return resolved unless scan_unresolved
44
+
45
+ unresolved = unresolved_namespaced_tokens(resolved, config: config, namespace: namespace)
46
+ return resolved if unresolved.empty?
47
+
48
+ raise UnresolvedTokensError, unresolved
49
+ end
50
+
51
+ # Sorted, deduplicated canonical keys of tokens in content whose first
52
+ # segment equals namespace.
53
+ #
54
+ # @return [Array<String>]
55
+ def unresolved_namespaced_tokens(content, config:, namespace:)
56
+ Token::Resolver::Document.new(content.to_s, config: config)
57
+ .token_keys
58
+ .select { |key| key.split(config.separators.first, 2).first == namespace }
59
+ .uniq
60
+ .sort
61
+ end
62
+
63
+ # Normalize replacements for the resolver: string keys and values.
64
+ #
65
+ # @return [Hash{String => String}]
66
+ def stringify_replacements(replacements)
67
+ replacements.to_h.transform_keys(&:to_s).transform_values(&:to_s)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.10'
8
+ VERSION = '7.1.11'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/ast/template.rb CHANGED
@@ -25,7 +25,11 @@ module Ast
25
25
  'rust' => 'Rust',
26
26
  'typescript' => 'TypeScript'
27
27
  }.freeze
28
+ autoload :FileTypes, 'ast/template/file_types'
29
+ autoload :MergeDispatch, 'ast/template/merge_dispatch'
28
30
  autoload :RustHostProvider, 'ast/template/rust_host_provider'
31
+ autoload :Strategies, 'ast/template/strategies'
32
+ autoload :TokenApplication, 'ast/template/token_application'
29
33
 
30
34
  class << self
31
35
  def rust_host_provider
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ast-template
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.10
4
+ version: 7.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,14 +43,34 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.10
46
+ version: 7.1.11
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.10
53
+ version: 7.1.11
54
+ - !ruby/object:Gem::Dependency
55
+ name: token-resolver
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.0.10
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '2.0'
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.0.10
54
74
  - !ruby/object:Gem::Dependency
55
75
  name: version_gem
56
76
  requirement: !ruby/object:Gem::Requirement
@@ -285,7 +305,11 @@ files:
285
305
  - README.md
286
306
  - lib/ast-template.rb
287
307
  - lib/ast/template.rb
308
+ - lib/ast/template/file_types.rb
309
+ - lib/ast/template/merge_dispatch.rb
288
310
  - lib/ast/template/rust_host_provider.rb
311
+ - lib/ast/template/strategies.rb
312
+ - lib/ast/template/token_application.rb
289
313
  - lib/ast/template/typed_core_bridge.rb
290
314
  - lib/ast/template/version.rb
291
315
  - sig/ast/template.rbs
@@ -295,10 +319,10 @@ licenses:
295
319
  - PolyForm-Small-Business-1.0.0
296
320
  metadata:
297
321
  homepage_uri: https://structuredmerge.org
298
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.10
299
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.10/CHANGELOG.md
322
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.11
323
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.11/CHANGELOG.md
300
324
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
301
- documentation_uri: https://www.rubydoc.info/gems/ast-template/7.1.10
325
+ documentation_uri: https://www.rubydoc.info/gems/ast-template/7.1.11
302
326
  funding_uri: https://github.com/sponsors/pboling
303
327
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
304
328
  news_uri: https://www.railsbling.com/tags/ast-template
metadata.gz.sig CHANGED
Binary file