natsuzora 0.4.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +55 -0
  4. data/CHANGELOG.md +62 -0
  5. data/Rakefile +75 -0
  6. data/lib/natsuzora/ast.rb +94 -0
  7. data/lib/natsuzora/context.rb +96 -0
  8. data/lib/natsuzora/contract/ast/any.rb +20 -0
  9. data/lib/natsuzora/contract/ast/list.rb +28 -0
  10. data/lib/natsuzora/contract/ast/node.rb +16 -0
  11. data/lib/natsuzora/contract/ast/record.rb +33 -0
  12. data/lib/natsuzora/contract/ast/ref.rb +27 -0
  13. data/lib/natsuzora/contract/ast/scalar.rb +60 -0
  14. data/lib/natsuzora/contract/ast.rb +38 -0
  15. data/lib/natsuzora/contract/compiled_lexer.rb +15 -0
  16. data/lib/natsuzora/contract/diff_marker.rb +15 -0
  17. data/lib/natsuzora/contract/document.rb +45 -0
  18. data/lib/natsuzora/contract/field.rb +62 -0
  19. data/lib/natsuzora/contract/parse_error.rb +16 -0
  20. data/lib/natsuzora/contract/parser.rb +362 -0
  21. data/lib/natsuzora/contract/scalar_type.rb +17 -0
  22. data/lib/natsuzora/contract/type_def.rb +39 -0
  23. data/lib/natsuzora/contract/type_ref_resolver.rb +56 -0
  24. data/lib/natsuzora/contract/validation_target.rb +13 -0
  25. data/lib/natsuzora/contract/validator.rb +179 -0
  26. data/lib/natsuzora/contract.rb +23 -0
  27. data/lib/natsuzora/data/lexers/contract.lkt1 +1 -0
  28. data/lib/natsuzora/data/lexers/template.lkt1 +1 -0
  29. data/lib/natsuzora/data_normalizable.rb +31 -0
  30. data/lib/natsuzora/errors.rb +37 -0
  31. data/lib/natsuzora/html_escape.rb +21 -0
  32. data/lib/natsuzora/lexer/compiled_lexer.rb +15 -0
  33. data/lib/natsuzora/lexer/token_processor.rb +156 -0
  34. data/lib/natsuzora/lexer.rb +95 -0
  35. data/lib/natsuzora/lexer_loader.rb +15 -0
  36. data/lib/natsuzora/lexers/contract.rb +24 -0
  37. data/lib/natsuzora/lexers/template.rb +31 -0
  38. data/lib/natsuzora/parser.rb +419 -0
  39. data/lib/natsuzora/payload.rb +35 -0
  40. data/lib/natsuzora/renderer.rb +132 -0
  41. data/lib/natsuzora/template.rb +34 -0
  42. data/lib/natsuzora/template_loader.rb +118 -0
  43. data/lib/natsuzora/token.rb +20 -0
  44. data/lib/natsuzora/validator.rb +73 -0
  45. data/lib/natsuzora/value.rb +73 -0
  46. data/lib/natsuzora/version.rb +5 -0
  47. data/lib/natsuzora.rb +30 -0
  48. metadata +105 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c392b524dfcc75df005176d7e8dc65337be7d94f26575f9b0816c40a7e061d4c
4
+ data.tar.gz: f660922c286c77a2b2ac6e863ae4a8f2427eb50ba7a70c50838d0d56e524c898
5
+ SHA512:
6
+ metadata.gz: 56c8ad664c64fb5f94992d1eadb163276c14494ab7c452b19d32d2a7fd81fcaedbf5eb89707cd0d9b0151b90ef7d20efce805a680b594bfd1be7be644e5d383d
7
+ data.tar.gz: 0f68bef0e391fc137904d96287d5daad54bbd29de293477b705138436bb5c6ff0efe80cbf94d584b2a69d459aa748418b7a5f43aeeacd501df6cef80ef0f6be9
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,55 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.2
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+
12
+ Layout/LineLength:
13
+ Max: 200
14
+
15
+ Metrics/BlockLength:
16
+ Exclude:
17
+ - 'spec/**/*'
18
+
19
+ Metrics/MethodLength:
20
+ Max: 50
21
+
22
+ Metrics/AbcSize:
23
+ Max: 40
24
+
25
+ Metrics/ClassLength:
26
+ Max: 400
27
+
28
+ Metrics/CyclomaticComplexity:
29
+ Max: 12
30
+
31
+ Metrics/PerceivedComplexity:
32
+ Max: 12
33
+
34
+ RSpec/DescribedClass:
35
+ EnforcedStyle: explicit
36
+
37
+ RSpec/ExampleLength:
38
+ Max: 25
39
+
40
+ RSpec/MultipleExpectations:
41
+ Max: 20
42
+
43
+ RSpec/SpecFilePathFormat:
44
+ Enabled: false
45
+
46
+ Capybara/RSpec/PredicateMatcher:
47
+ Enabled: false
48
+
49
+ Style/IfUnlessModifier:
50
+ Enabled: false
51
+
52
+ Style/MultipleComparison:
53
+ Enabled: false
54
+
55
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ ### Added
6
+ - `Natsuzora::Contract` namespace which absorbs the formerly separate `subaru` gem.
7
+ Provides parsing of `.ntzc` contract notation and validation of JSON data
8
+ against contracts. Two-generation diff markers (`+`, `-`, `*`) are supported.
9
+ - Module-level helpers: `Natsuzora::Contract.parse(input)`,
10
+ `Natsuzora::Contract.parse_file(input)`,
11
+ `Natsuzora::Contract.parse_file_with_diff(input)`,
12
+ `Natsuzora::Contract.validate(contract, data)`,
13
+ `Natsuzora::Contract.validate_with_target(document, data, target:)`.
14
+ - Contract AST nodes are grouped under `Natsuzora::Contract::AST::`
15
+ (`Any`, `Scalar`, `Record`, `List`, `Ref`, with `Node` as the abstract
16
+ base). `Natsuzora::Contract::AST.from_h(hash)` rebuilds an AST tree
17
+ from its hash representation.
18
+ - `Natsuzora::Contract::TypeRefResolver` walks an AST tree replacing
19
+ `AST::Ref` nodes with concrete contracts; configurable via
20
+ `on_missing` / `on_unavailable` / `on_cyclic` callbacks. Cyclic type
21
+ references (e.g. `type A { ref: B }; type B { ref: A }`) raise via
22
+ `on_cyclic` instead of recursing forever.
23
+ - Resource limits to bound pathological input:
24
+ - `Natsuzora::Renderer::MAX_RENDER_DEPTH` (1024) — caps recursion
25
+ through nested `{[#if]}` / `{[#each]}` / `{[!include]}` blocks.
26
+ - `Natsuzora::Renderer::MAX_OUTPUT_BYTES` (50 MiB) — caps the
27
+ rendered output size, checked per `{[#each]}` iteration.
28
+ - `Natsuzora::Contract::Validator::MAX_VALIDATE_DEPTH` (64) — caps
29
+ nesting depth of validated data trees.
30
+ Exceeding any limit raises `Natsuzora::RenderError` or
31
+ `Natsuzora::Contract::ValidationError` with a descriptive message.
32
+ - Shared JSON tests under `tests/contract/` for cross-language parity with
33
+ the Rust `natsuzora-contract` crate.
34
+ - `Natsuzora::Payload` class wrapping render input data. The class
35
+ encapsulates the boundary between untrusted host data and Natsuzora's
36
+ internal value space (Symbol→String key adaptation, whole-number
37
+ Float→Integer coercion, plus type/range validation).
38
+ - `Natsuzora::DataNormalizable` mixin providing `normalize_data(data)` for
39
+ pure data adaptation (no exceptions raised).
40
+ - `Natsuzora::Validator.validate_data!(data)` for asserting that a
41
+ prepared value conforms to the Natsuzora type system.
42
+
43
+ ### Changed
44
+ - **Breaking** (pre-1.0): `Natsuzora::Template#render` now accepts a
45
+ `Natsuzora::Payload` instead of a raw `Hash`. The top-level facade
46
+ `Natsuzora.render(source, data, ...)` is unchanged and wraps `data` in a
47
+ Payload internally; only callers that construct a `Template` directly
48
+ via `Natsuzora.parse` need to update.
49
+
50
+ ```ruby
51
+ # Before
52
+ template = Natsuzora.parse(source)
53
+ template.render(hash)
54
+
55
+ # After
56
+ template = Natsuzora.parse(source)
57
+ template.render(Natsuzora::Payload.new(hash))
58
+ ```
59
+
60
+ ## 0.2.0
61
+
62
+ Initial release of the natsuzora template language.
data/Rakefile ADDED
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'fileutils'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+
8
+ LEXER_ROOT = File.expand_path(__dir__)
9
+ LEXERS = {
10
+ contract: {
11
+ source: File.join(LEXER_ROOT, 'lib', 'natsuzora', 'lexers', 'contract.rb'),
12
+ output: File.join(LEXER_ROOT, 'lib', 'natsuzora', 'data', 'lexers', 'contract.lkt1')
13
+ },
14
+ template: {
15
+ source: File.join(LEXER_ROOT, 'lib', 'natsuzora', 'lexers', 'template.rb'),
16
+ output: File.join(LEXER_ROOT, 'lib', 'natsuzora', 'data', 'lexers', 'template.lkt1')
17
+ }
18
+ }.freeze
19
+
20
+ RSpec::Core::RakeTask.new(:spec)
21
+ RuboCop::RakeTask.new
22
+
23
+ def build_program(source)
24
+ require 'lexer_kit'
25
+ LexerKit.load_builder(source).compile
26
+ end
27
+
28
+ def compile_lexers
29
+ require 'lexer_kit'
30
+
31
+ LEXERS.each do |name, config|
32
+ FileUtils.mkdir_p(File.dirname(config[:output]))
33
+ LexerKit::Format::LKT1.save(build_program(config[:source]), path: config[:output])
34
+ puts "Compiled #{name} lexer to #{config[:output]}"
35
+ end
36
+ end
37
+
38
+ def lexer_check_failures
39
+ require 'lexer_kit'
40
+
41
+ LEXERS.each_with_object([]) do |(name, config), failures|
42
+ unless File.file?(config[:output])
43
+ failures << "Missing #{name} lexer: #{config[:output]}"
44
+ next
45
+ end
46
+
47
+ expected_binary = build_program(config[:source]).to_binary
48
+ actual_binary = LexerKit.load_lexer(config[:output]).to_binary
49
+ failures << "Stale #{name} lexer: #{config[:output]}" unless actual_binary == expected_binary
50
+ rescue StandardError => e
51
+ failures << "Invalid #{name} lexer: #{e.class}: #{e.message}"
52
+ end
53
+ end
54
+
55
+ def abort_if_lexers_stale
56
+ failures = lexer_check_failures
57
+ return if failures.empty?
58
+
59
+ failures.each { |failure| warn failure }
60
+ abort 'Precompiled lexer files are not up to date. Run `rake lexers:compile`.'
61
+ end
62
+
63
+ namespace :lexers do
64
+ desc 'Compile LexerKit DSL files into precompiled .lkt1 programs'
65
+ task :compile do
66
+ compile_lexers
67
+ end
68
+
69
+ desc 'Verify precompiled LexerKit programs are present and up to date'
70
+ task :check do
71
+ abort_if_lexers_stale
72
+ end
73
+ end
74
+
75
+ task default: %i[lexers:check spec rubocop]
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Natsuzora
4
+ module AST
5
+ class Node
6
+ attr_reader :line, :column
7
+
8
+ def initialize(line: nil, column: nil)
9
+ @line = line
10
+ @column = column
11
+ end
12
+ end
13
+
14
+ class Template < Node
15
+ attr_reader :nodes
16
+
17
+ def initialize(nodes, **)
18
+ super(**)
19
+ @nodes = nodes
20
+ end
21
+ end
22
+
23
+ class Text < Node
24
+ attr_reader :content
25
+
26
+ def initialize(content, **)
27
+ super(**)
28
+ @content = content
29
+ end
30
+ end
31
+
32
+ class Variable < Node
33
+ attr_reader :path, :modifier
34
+
35
+ # modifier: nil (default), :nullable (?), :required (!)
36
+ def initialize(path, modifier: nil, **)
37
+ super(**)
38
+ @path = path
39
+ @modifier = modifier
40
+ end
41
+ end
42
+
43
+ class IfBlock < Node
44
+ attr_reader :condition, :then_nodes, :else_nodes
45
+
46
+ def initialize(condition:, then_nodes:, else_nodes: nil, **)
47
+ super(**)
48
+ @condition = condition
49
+ @then_nodes = then_nodes
50
+ @else_nodes = else_nodes
51
+ end
52
+ end
53
+
54
+ class UnlessBlock < Node
55
+ attr_reader :condition, :body_nodes
56
+
57
+ def initialize(condition:, body_nodes:, **)
58
+ super(**)
59
+ @condition = condition
60
+ @body_nodes = body_nodes
61
+ end
62
+ end
63
+
64
+ class EachBlock < Node
65
+ attr_reader :collection, :item_name, :body_nodes
66
+
67
+ def initialize(collection:, item_name:, body_nodes:, **)
68
+ super(**)
69
+ @collection = collection
70
+ @item_name = item_name
71
+ @body_nodes = body_nodes
72
+ end
73
+ end
74
+
75
+ class UnsecureOutput < Node
76
+ attr_reader :path
77
+
78
+ def initialize(path:, **)
79
+ super(**)
80
+ @path = path
81
+ end
82
+ end
83
+
84
+ class Include < Node
85
+ attr_reader :name, :args
86
+
87
+ def initialize(name:, args:, **)
88
+ super(**)
89
+ @name = name
90
+ @args = args
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Natsuzora
4
+ # Manages variable scope and resolution during rendering.
5
+ class Context
6
+ def initialize(root_data)
7
+ raise Natsuzora::TypeError, 'Root data must be an object' unless root_data.is_a?(Hash)
8
+
9
+ @root = root_data
10
+ @local_stack = []
11
+ end
12
+
13
+ def resolve(variable)
14
+ path = variable.path
15
+ line = variable.line
16
+ column = variable.column
17
+
18
+ name = path.first
19
+ raise UndefinedVariableError.new('Undefined variable: <empty path>', line: line, column: column) unless name
20
+
21
+ value = resolve_name(name, line: line, column: column)
22
+
23
+ path[1..].each do |segment|
24
+ value = access_property(value, segment, line: line, column: column)
25
+ end
26
+
27
+ value
28
+ end
29
+
30
+ def push_scope(bindings = {})
31
+ validate_no_shadowing!(bindings)
32
+ @local_stack.push(bindings)
33
+ end
34
+
35
+ def push_include_scope(bindings)
36
+ @local_stack.push(bindings)
37
+ end
38
+
39
+ def pop_scope
40
+ @local_stack.pop
41
+ end
42
+
43
+ def with_scope(bindings, include_scope: false)
44
+ if include_scope
45
+ push_include_scope(bindings)
46
+ else
47
+ push_scope(bindings)
48
+ end
49
+ yield
50
+ ensure
51
+ pop_scope
52
+ end
53
+
54
+ private
55
+
56
+ def resolve_name(name, line: nil, column: nil)
57
+ @local_stack.reverse_each do |scope|
58
+ return scope[name] if scope.key?(name)
59
+ end
60
+
61
+ return @root[name] if @root.key?(name)
62
+
63
+ raise UndefinedVariableError.new("Undefined variable: #{name}", line: line, column: column)
64
+ end
65
+
66
+ def validate_no_shadowing!(bindings)
67
+ bindings.each_key do |name|
68
+ name_str = name.to_s
69
+ origin = binding_origin(name_str)
70
+ next unless origin
71
+
72
+ raise ShadowingError,
73
+ "Cannot shadow existing variable '#{name_str}' (already defined in #{origin})"
74
+ end
75
+ end
76
+
77
+ def binding_origin(name)
78
+ return 'root data' if @root.key?(name)
79
+
80
+ @local_stack.reverse_each do |scope|
81
+ return 'outer local scope' if scope.key?(name)
82
+ end
83
+ nil
84
+ end
85
+
86
+ def access_property(value, key, line: nil, column: nil)
87
+ raise TypeError, "Cannot access property '#{key}' on non-object" unless value.is_a?(Hash)
88
+
89
+ unless value.key?(key)
90
+ raise UndefinedVariableError.new("Undefined property: #{key}", line: line, column: column)
91
+ end
92
+
93
+ value[key]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+
5
+ module Natsuzora
6
+ module Contract
7
+ module AST
8
+ # Any value (unconstrained).
9
+ class Any < Node
10
+ def to_h
11
+ { 'kind' => 'any' }
12
+ end
13
+
14
+ def ==(other)
15
+ other.is_a?(Any)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+
5
+ module Natsuzora
6
+ module Contract
7
+ module AST
8
+ # Ordered list of items of a single contract type.
9
+ # Serialized as 'kind' => 'array' for JSON compatibility.
10
+ class List < Node
11
+ attr_reader :items
12
+
13
+ def initialize(items)
14
+ super()
15
+ @items = items
16
+ end
17
+
18
+ def to_h
19
+ { 'kind' => 'array', 'items' => @items.to_h }
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(List) && other.items == @items
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Natsuzora
4
+ module Contract
5
+ module AST
6
+ # Base class for contract AST nodes.
7
+ # Each subclass implements +to_h+ for JSON serialization.
8
+ # Use +Natsuzora::Contract::AST.from_h+ for deserialization.
9
+ class Node
10
+ def to_h
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+
5
+ module Natsuzora
6
+ module Contract
7
+ module AST
8
+ # Record-style object with named properties.
9
+ # Serialized as 'kind' => 'object' for JSON compatibility.
10
+ class Record < Node
11
+ attr_reader :properties, :required
12
+
13
+ def initialize(properties = {}, required = [])
14
+ super()
15
+ @properties = properties
16
+ @required = required
17
+ end
18
+
19
+ def to_h
20
+ h = { 'kind' => 'object', 'properties' => @properties.transform_values(&:to_h) }
21
+ h['required'] = @required unless @required.empty?
22
+ h
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(Record) &&
27
+ other.properties == @properties &&
28
+ other.required == @required
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+
5
+ module Natsuzora
6
+ module Contract
7
+ module AST
8
+ # Type reference (used during parsing, resolved before use).
9
+ class Ref < Node
10
+ attr_reader :name
11
+
12
+ def initialize(name)
13
+ super()
14
+ @name = name
15
+ end
16
+
17
+ def to_h
18
+ { 'kind' => 'ref', 'name' => @name }
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(Ref) && other.name == @name
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+ require_relative '../scalar_type'
5
+
6
+ module Natsuzora
7
+ module Contract
8
+ module AST
9
+ # Scalar value with type and modifier.
10
+ class Scalar < Node
11
+ # Modifier for null/empty handling on scalar values.
12
+ module Modifier
13
+ # Default: null not allowed, empty string allowed
14
+ NONE = :none
15
+ # `?` modifier: null allowed
16
+ NULLABLE = :nullable
17
+ # `!` modifier: null not allowed, empty string not allowed
18
+ REQUIRED = :required
19
+ end
20
+
21
+ attr_reader :scalar_type, :modifier
22
+
23
+ def initialize(scalar_type, modifier = Modifier::NONE)
24
+ super()
25
+ @scalar_type = scalar_type
26
+ @modifier = modifier
27
+ end
28
+
29
+ def nullable?
30
+ @modifier == Modifier::NULLABLE
31
+ end
32
+
33
+ def required?
34
+ @modifier == Modifier::REQUIRED
35
+ end
36
+
37
+ def accepts?(data)
38
+ case @scalar_type
39
+ when ScalarType::STRING then data.is_a?(String)
40
+ when ScalarType::INTEGER then data.is_a?(Integer)
41
+ when ScalarType::BOOL then data.is_a?(TrueClass) || data.is_a?(FalseClass)
42
+ when ScalarType::SCALAR then data.is_a?(String) || data.is_a?(Integer)
43
+ end
44
+ end
45
+
46
+ def to_h
47
+ h = { 'kind' => 'scalar', 'type' => @scalar_type.to_s }
48
+ h['modifier'] = @modifier.to_s unless @modifier == Modifier::NONE
49
+ h
50
+ end
51
+
52
+ def ==(other)
53
+ other.is_a?(Scalar) &&
54
+ other.scalar_type == @scalar_type &&
55
+ other.modifier == @modifier
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ast/node'
4
+ require_relative 'ast/any'
5
+ require_relative 'ast/scalar'
6
+ require_relative 'ast/record'
7
+ require_relative 'ast/list'
8
+ require_relative 'ast/ref'
9
+
10
+ module Natsuzora
11
+ module Contract
12
+ # Contract AST nodes and their serialization helpers.
13
+ module AST
14
+ # Build an AST::Node tree from its hash representation.
15
+ def self.from_h(hash)
16
+ case hash['kind']
17
+ when 'any'
18
+ Any.new
19
+ when 'scalar'
20
+ scalar_type = hash['type'].to_sym
21
+ modifier = (hash['modifier'] || 'none').to_sym
22
+ Scalar.new(scalar_type, modifier)
23
+ when 'object'
24
+ required = hash['required'] || []
25
+ properties = (hash['properties'] || {}).transform_values { |v| from_h(v) }
26
+ Record.new(properties, required)
27
+ when 'array'
28
+ items = from_h(hash['items'])
29
+ List.new(items)
30
+ when 'ref'
31
+ Ref.new(hash['name'])
32
+ else
33
+ raise ArgumentError, "Unknown contract kind: #{hash['kind']}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lexer_loader'
4
+
5
+ module Natsuzora
6
+ module Contract
7
+ module CompiledLexer
8
+ LEXER_PATH = File.expand_path('../data/lexers/contract.lkt1', __dir__)
9
+
10
+ def self.instance
11
+ @instance ||= LexerLoader.load_compiled(LEXER_PATH)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Natsuzora
4
+ module Contract
5
+ # Diff marker for 2-generation contracts.
6
+ module DiffMarker
7
+ # `+` - Field will be added in next generation
8
+ ADDED = :added
9
+ # `-` - Field will be removed in next generation
10
+ REMOVED = :removed
11
+ # `*` - Field type will change in next generation
12
+ CHANGED = :changed
13
+ end
14
+ end
15
+ end