schemagraphy 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,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaGraphy
4
+ # A utility module for introspecting schema definitions.
5
+ # Provides methods for retrieving metadata, default values, and type information
6
+ # from a schema hash using a dot-separated path syntax.
7
+ module SchemaUtils
8
+ module_function
9
+
10
+ # Retrieve a nested property definition from a schema using a dot-separated path.
11
+ #
12
+ # @example Schema Structure
13
+ # schema = {
14
+ # "$schema": {
15
+ # "properties": {
16
+ # "property1": {
17
+ # "properties": {
18
+ # "subproperty1": {
19
+ # "default": "value1",
20
+ # "type": "String"
21
+ # }
22
+ # }
23
+ # }
24
+ # }
25
+ # }
26
+ # }
27
+ # crawl_properties(schema, "property1.subproperty1")
28
+ # # => { "default" => "value1", "type" => "String" }
29
+ #
30
+ # @param schema [Hash] The schema hash to crawl.
31
+ # @param path [String] The dot-separated path to the property.
32
+ # @return [Hash, nil] The property definition hash, or `nil` if not found.
33
+ def crawl_properties schema, path
34
+ path_components = path.split('.')
35
+ current = schema['$schema'] || schema
36
+
37
+ path_components.each do |component|
38
+ return nil unless current.is_a?(Hash)
39
+ return nil unless current['properties']&.key?(component)
40
+
41
+ current = current['properties'][component]
42
+ end
43
+
44
+ current
45
+ end
46
+
47
+ # Get the default value for a property from the schema.
48
+ #
49
+ # @param schema [Hash] The schema hash.
50
+ # @param path [String] The dot-separated path to the property.
51
+ # @return [Object, nil] The default value, or `nil` if not defined.
52
+ def default_for schema, path
53
+ property = crawl_properties(schema, path)
54
+ return nil unless property.is_a?(Hash)
55
+
56
+ property['default'] || property['dflt']
57
+ end
58
+
59
+ # Get the type for a property from the schema.
60
+ #
61
+ # @param schema [Hash] The schema hash.
62
+ # @param path [String] The dot-separated path to the property.
63
+ # @return [String, nil] The property type, or `nil` if not defined.
64
+ def type_for schema, path
65
+ property = crawl_properties(schema, path)
66
+ return nil unless property.is_a?(Hash)
67
+
68
+ property['type']
69
+ end
70
+
71
+ # Get the templating configuration for a property from the schema.
72
+ #
73
+ # @param schema [Hash] The schema hash.
74
+ # @param path [String] The dot-separated path to the property.
75
+ # @return [Hash] The templating configuration hash.
76
+ def templating_config_for schema, path
77
+ property = crawl_properties(schema, path)
78
+ return {} unless property.is_a?(Hash)
79
+
80
+ return property['templating'] if property['templating']
81
+
82
+ if property['type'].to_s.downcase == 'liquid'
83
+ { 'default' => 'liquid', 'delay' => true }
84
+ elsif property['type'].to_s.downcase == 'erb'
85
+ { 'default' => 'erb', 'delay' => true }
86
+ else
87
+ {}
88
+ end
89
+ end
90
+
91
+ # Check if a property is a templated field.
92
+ #
93
+ # @param schema [Hash] The schema hash.
94
+ # @param path [String] The dot-separated path to the property.
95
+ # @return [Boolean] `true` if the field has templating configured, `false` otherwise.
96
+ def templated_field? schema, path
97
+ property = crawl_properties(schema, path)
98
+ return false unless property.is_a?(Hash)
99
+
100
+ property.key?('templating') && property['templating'].is_a?(Hash)
101
+ end
102
+
103
+ # Crawl the schema to find the metadata for a given path.
104
+ #
105
+ # @param schema [Hash] The schema hash.
106
+ # @param path [String, nil] The dot-separated path.
107
+ # @return [Hash] The metadata hash.
108
+ def self.crawl_meta schema, path = nil
109
+ parts = path ? path.split('.') : []
110
+ node = schema['$schema'] || schema
111
+ meta = {}
112
+
113
+ parts.each do |part|
114
+ node = node['properties'][part] if node['properties']&.key?(part)
115
+ break unless node.is_a?(Hash)
116
+
117
+ # Only update meta if this level has it
118
+ meta = node if node['templating']
119
+ end
120
+
121
+ meta['$meta'] || meta['sgyml'] || meta['templating'] || {}
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaGraphy
4
+ # Provides SGYML (SchemaGraphy YAML-based Modeling Language) type classification.
5
+ # This is the canonical source for SGYML data types used across DocOps Lab tooling.
6
+ #
7
+ # Type strings are in "Kind:Class" form, e.g. "Scalar:String", "Compound:ArrayList",
8
+ # "Null:nil". Downstream gems (AsciiSourcerer, ReleaseHx, etc.) that render SGYML
9
+ # templates should obtain this classification via SchemaGraphy rather than
10
+ # re-implementing it.
11
+ module SGYML
12
+ module_function
13
+
14
+ # Classifies a Ruby value by its SGYML type.
15
+ # @param input [Object] Any Ruby value.
16
+ # @return [String] A "Kind:Class" type string.
17
+ def classify input
18
+ if input.nil?
19
+ 'Null:nil'
20
+ elsif input.is_a?(Array)
21
+ classify_array(input)
22
+ elsif input.is_a?(Hash)
23
+ classify_hash(input)
24
+ elsif input.is_a?(String)
25
+ 'Scalar:String'
26
+ elsif input.is_a?(Integer)
27
+ 'Scalar:Number'
28
+ elsif input.is_a?(Float)
29
+ 'Scalar:Float'
30
+ elsif input.is_a?(Time)
31
+ 'Scalar:DateTime'
32
+ elsif input.is_a?(TrueClass) || input.is_a?(FalseClass)
33
+ 'Scalar:Boolean'
34
+ else
35
+ 'unknown:unknown'
36
+ end
37
+ end
38
+
39
+ def classify_array input
40
+ if input.all? do |i|
41
+ i.is_a?(Integer) || i.is_a?(Float) || i.is_a?(String) || i.is_a?(TrueClass) || i.is_a?(FalseClass)
42
+ end
43
+ 'Compound:ArrayList'
44
+ elsif input.all? { |i| i.is_a?(Hash) && i.keys.length >= 2 }
45
+ 'Compound:ArrayTable'
46
+ else
47
+ 'Compound:Array'
48
+ end
49
+ end
50
+
51
+ def classify_hash input
52
+ if input.values.all? { |v| v.is_a?(Hash) && v.keys.length >= 2 }
53
+ 'Compound:MapTable'
54
+ else
55
+ 'Compound:Map'
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaGraphy
4
+ # A utility module for working with the custom tag data structure.
5
+ # The structure is a hash with 'value' and '__tag__' keys.
6
+ module TagUtils
7
+ # Extracts the original value from a tagged data structure.
8
+ #
9
+ # @param value [Object] The tagged value (a Hash) or any other value.
10
+ # @return [Object] The original value, or the value itself if not tagged.
11
+ def self.detag value
12
+ value.is_a?(Hash) && value.key?('value') ? value['value'] : value
13
+ end
14
+
15
+ # Retrieves the tag from a tagged data structure.
16
+ #
17
+ # @param value [Object] The tagged value (a Hash) or any other value.
18
+ # @return [String, nil] The tag string, or `nil` if not tagged.
19
+ def self.tag_of value
20
+ value.is_a?(Hash) ? value['__tag__'] : nil
21
+ end
22
+
23
+ # Checks if a value has a specific tag.
24
+ #
25
+ # @param value [Object] The tagged value to check.
26
+ # @param tag [String, Symbol] The tag to check for.
27
+ # @return [Boolean] `true` if the value has the specified tag, `false` otherwise.
28
+ def self.tag? value, tag
29
+ tag_of(value)&.to_s == tag.to_s
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tilt'
4
+ require 'sourcerer/templating'
5
+
6
+ # frozen_string_literal: true
7
+
8
+ module SchemaGraphy
9
+ # A module for handling templated fields within a data structure based on a schema or definition.
10
+ # It provides methods for pre-compiling and rendering fields using various template engines.
11
+ module Templating
12
+ extend Sourcerer::Templating
13
+
14
+ # Renders a field if it is a template.
15
+ #
16
+ # @param field [Object] The field to render.
17
+ # @param context [Hash] The context to use for rendering.
18
+ # @return [Object] The rendered field, or the original field if it's not a template.
19
+ def self.resolve_field field, context = {}
20
+ render_field_if_template(field, context)
21
+ end
22
+
23
+ # Recursively pre-compiles templated fields in a data structure based on a schema.
24
+ #
25
+ # @param data [Hash] The data to process.
26
+ # @param schema [Hash] The schema defining which fields are templated.
27
+ # @param base_path [String] The base path for the current data level.
28
+ # @param scope [Hash] The scope to use for compilation.
29
+ def self.precompile_from_schema! data, schema, base_path = '', scope: {}
30
+ return unless data.is_a?(Hash)
31
+
32
+ data.each do |key, value|
33
+ path = [base_path, key].reject(&:empty?).join('.')
34
+
35
+ precompile_from_schema!(value, schema, path, scope: scope) if value.is_a?(Hash)
36
+
37
+ next unless SchemaGraphy::SchemaUtils.templated_field?(schema, path)
38
+
39
+ compile_templated_fields!(
40
+ data: data,
41
+ schema: schema,
42
+ fields: [{ key: key, path: path }],
43
+ scope: scope)
44
+ end
45
+ end
46
+
47
+ # An alias for the `Sourcerer::Templating::TemplatedField` class.
48
+ TemplatedField = Sourcerer::Templating::TemplatedField
49
+
50
+ # Compiles templated fields in the data.
51
+ #
52
+ # @param data [Hash] The data containing the fields to compile.
53
+ # @param schema [Hash] The schema definition.
54
+ # @param fields [Array<Hash>] An array of fields to compile, each with a `:key` and `:path`.
55
+ # @param scope [Hash] The scope to use for compilation.
56
+ def self.compile_templated_fields! data:, schema:, fields:, scope: {}
57
+ fields.each do |entry|
58
+ key = entry[:key]
59
+ path = entry[:path]
60
+ val = data[key]
61
+
62
+ next unless val.is_a?(String) || (val.is_a?(Hash) && val['__tag__'] && val['value'])
63
+
64
+ raw = val.is_a?(Hash) ? val['value'] : val
65
+ tagged = val.is_a?(Hash)
66
+ config = SchemaGraphy::SchemaUtils.templating_config_for(schema, path)
67
+ engine = tagged ? val['__tag__'] : (config['default'] || 'liquid')
68
+
69
+ compiled = Sourcerer::Templating::Engines.compile(raw, engine)
70
+
71
+ data[key] = if config['delay']
72
+ Sourcerer::Templating::TemplatedField.new(raw, compiled, engine, tagged, inferred: !tagged)
73
+ else
74
+ Sourcerer::Templating::Engines.render(compiled, engine, scope)
75
+ end
76
+ end
77
+ end
78
+
79
+ # Recursively renders all pre-compiled templated fields in a data structure.
80
+ #
81
+ # @param data [Hash, Array] The data structure to process.
82
+ # @param context [Hash] The context to use for rendering.
83
+ def self.render_all_templated_fields! data, context = {}
84
+ return unless data.is_a?(Hash)
85
+
86
+ data.each do |key, value|
87
+ case value
88
+ when TemplatedField
89
+ data[key] = value.render(context)
90
+ when Hash
91
+ render_all_templated_fields!(value, context)
92
+ when Array
93
+ value.each_with_index do |item, idx|
94
+ if item.is_a?(TemplatedField)
95
+ value[idx] = item.render(context)
96
+ elsif item.is_a?(Hash)
97
+ render_all_templated_fields!(item, context)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchemaGraphy
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schemagraphy/version'
4
+ require_relative 'schemagraphy/loader'
5
+ require_relative 'schemagraphy/tag_utils'
6
+ require_relative 'schemagraphy/schema_utils'
7
+ require_relative 'schemagraphy/templating'
8
+ require_relative 'schemagraphy/regexp_utils'
9
+ require_relative 'schemagraphy/safe_expression'
10
+ require_relative 'schemagraphy/cfgyml/doc_builder'
11
+ require_relative 'schemagraphy/data_query/json_pointer'
12
+ require_relative 'schemagraphy/cfgyml/path_reference'
13
+ require_relative 'schemagraphy/sgyml'
14
+ require_relative 'schemagraphy/liquid/filters'
15
+
16
+ module SchemaGraphy
17
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schemagraphy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - DocOpsLab
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: asciisourcerer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: prism
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.20'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.20'
40
+ - !ruby/object:Gem::Dependency
41
+ name: psych
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.3.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.3.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: tilt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.3'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: to_regexp
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.2'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: yaml
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.4'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.4'
96
+ description: A YAML-based schema system for validating and transforming configuration
97
+ data
98
+ email:
99
+ - docopslab@protonmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - README.adoc
105
+ - lib/schemagraphy.rb
106
+ - lib/schemagraphy/attribute_resolver.rb
107
+ - lib/schemagraphy/cfgyml/definition.rb
108
+ - lib/schemagraphy/cfgyml/doc_builder.rb
109
+ - lib/schemagraphy/cfgyml/path_reference.rb
110
+ - lib/schemagraphy/cfgyml/templates/config-property.adoc.liquid
111
+ - lib/schemagraphy/cfgyml/templates/config-reference.adoc.liquid
112
+ - lib/schemagraphy/cfgyml/templates/sample-config.yaml.liquid
113
+ - lib/schemagraphy/cfgyml/templates/sample-property.yaml.liquid
114
+ - lib/schemagraphy/data_query/json_pointer.rb
115
+ - lib/schemagraphy/liquid/filters.rb
116
+ - lib/schemagraphy/loader.rb
117
+ - lib/schemagraphy/regexp_utils.rb
118
+ - lib/schemagraphy/safe_expression.rb
119
+ - lib/schemagraphy/schema_utils.rb
120
+ - lib/schemagraphy/sgyml.rb
121
+ - lib/schemagraphy/tag_utils.rb
122
+ - lib/schemagraphy/templating.rb
123
+ - lib/schemagraphy/version.rb
124
+ homepage: https://github.com/DocOps/schemagraphy
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ allowed_push_host: https://rubygems.org
129
+ rubygems_mfa_required: 'true'
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 3.2.0
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.7.2
145
+ specification_version: 4
146
+ summary: Schema-driven configuration and data validation library
147
+ test_files: []