verquest 0.1.0 → 0.2.1
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 +4 -4
- data/.yardopts +8 -0
- data/CHANGELOG.md +9 -0
- data/README.md +416 -13
- data/lib/verquest/base/helper_class_methods.rb +37 -0
- data/lib/verquest/base/private_class_methods.rb +247 -0
- data/lib/verquest/base/public_class_methods.rb +108 -0
- data/lib/verquest/base.rb +38 -0
- data/lib/verquest/configuration.rb +73 -0
- data/lib/verquest/gem_version.rb +5 -0
- data/lib/verquest/properties/array.rb +63 -0
- data/lib/verquest/properties/base.rb +104 -0
- data/lib/verquest/properties/collection.rb +147 -0
- data/lib/verquest/properties/field.rb +65 -0
- data/lib/verquest/properties/object.rb +83 -0
- data/lib/verquest/properties/reference.rb +92 -0
- data/lib/verquest/properties.rb +47 -0
- data/lib/verquest/result.rb +75 -0
- data/lib/verquest/transformer.rb +179 -0
- data/lib/verquest/version.rb +208 -1
- data/lib/verquest/version_resolver.rb +42 -0
- data/lib/verquest/versions.rb +65 -0
- data/lib/verquest.rb +96 -3
- metadata +46 -9
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
module Properties
|
5
|
+
# Collection property type for arrays of objects
|
6
|
+
#
|
7
|
+
# Represents an array of complex objects in the schema.
|
8
|
+
# Used for defining collections of structured data objects.
|
9
|
+
#
|
10
|
+
# @example Define a collection of items with inline properties
|
11
|
+
# products = Verquest::Properties::Collection.new(name: :products)
|
12
|
+
# products.add(Verquest::Properties::Field.new(name: :id, type: :string, required: true))
|
13
|
+
# products.add(Verquest::Properties::Field.new(name: :name, type: :string))
|
14
|
+
#
|
15
|
+
# @example Define a collection referencing an existing schema
|
16
|
+
# products = Verquest::Properties::Collection.new(
|
17
|
+
# name: :products,
|
18
|
+
# item: ProductRequest
|
19
|
+
# )
|
20
|
+
class Collection < Base
|
21
|
+
# Initialize a new Collection property
|
22
|
+
#
|
23
|
+
# @param name [Symbol] The name of the property
|
24
|
+
# @param item [Verquest::Base, nil] Optional reference to an external schema class
|
25
|
+
# @param required [Boolean] Whether this property is required
|
26
|
+
# @param map [String, nil] The mapping path for this property
|
27
|
+
# @param schema_options [Hash] Additional JSON schema options for this property
|
28
|
+
# @raise [ArgumentError] If attempting to map a collection to the root
|
29
|
+
def initialize(name:, item: nil, required: false, map: nil, **schema_options)
|
30
|
+
raise ArgumentError, "You can not map collection to the root" if map == "/"
|
31
|
+
|
32
|
+
@properties = {}
|
33
|
+
|
34
|
+
@name = name
|
35
|
+
@item = item
|
36
|
+
@required = required
|
37
|
+
@map = map
|
38
|
+
@schema_options = schema_options
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add a child property to this collection's item definition
|
42
|
+
#
|
43
|
+
# @param property [Verquest::Properties::Base] The property to add to the collection items
|
44
|
+
# @return [Verquest::Properties::Base] The added property
|
45
|
+
def add(property)
|
46
|
+
properties[property.name] = property
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if this collection references an external item schema
|
50
|
+
#
|
51
|
+
# @return [Boolean] True if the collection uses an external reference
|
52
|
+
def has_item?
|
53
|
+
!item.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Generate JSON schema definition for this collection property
|
57
|
+
#
|
58
|
+
# @return [Hash] The schema definition for this collection property
|
59
|
+
def to_schema
|
60
|
+
if has_item?
|
61
|
+
{
|
62
|
+
name => {
|
63
|
+
type: :array,
|
64
|
+
items: {
|
65
|
+
"$ref": item.to_ref
|
66
|
+
}
|
67
|
+
}.merge(schema_options)
|
68
|
+
}
|
69
|
+
else
|
70
|
+
{
|
71
|
+
name => {
|
72
|
+
type: :array,
|
73
|
+
items: {
|
74
|
+
type: :object,
|
75
|
+
required: properties.values.select(&:required).map(&:name),
|
76
|
+
properties: properties.transform_values { |property| property.to_schema[property.name] }
|
77
|
+
}
|
78
|
+
}.merge(schema_options)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Generate validation schema for this collection property
|
84
|
+
#
|
85
|
+
# @param version [String, nil] The version to generate validation schema for
|
86
|
+
# @return [Hash] The validation schema for this collection property
|
87
|
+
def to_validation_schema(version: nil)
|
88
|
+
if has_item?
|
89
|
+
{
|
90
|
+
name => {
|
91
|
+
type: :array,
|
92
|
+
items: item.to_validation_schema(version: version)
|
93
|
+
}.merge(schema_options)
|
94
|
+
}
|
95
|
+
else
|
96
|
+
{
|
97
|
+
name => {
|
98
|
+
type: :array,
|
99
|
+
items: {
|
100
|
+
type: :object,
|
101
|
+
required: properties.values.select(&:required).map(&:name),
|
102
|
+
properties: properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
|
103
|
+
}
|
104
|
+
}.merge(schema_options)
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Create mapping for this collection property and all its children
|
110
|
+
#
|
111
|
+
# This method handles two different scenarios:
|
112
|
+
# 1. When the collection references an external item schema (`has_item?` returns true)
|
113
|
+
# - Creates mappings by transforming keys from the referenced item schema
|
114
|
+
# - Adds array notation ([]) to indicate this is a collection
|
115
|
+
# - Prefixes all keys and values with the appropriate paths
|
116
|
+
#
|
117
|
+
# 2. When the collection has inline item properties
|
118
|
+
# - Creates mappings for each property in the collection items
|
119
|
+
# - Each property gets mapped with array notation and appropriate prefixes
|
120
|
+
#
|
121
|
+
# @param key_prefix [Array<Symbol>] Prefix for the source key
|
122
|
+
# @param value_prefix [Array<String>] Prefix for the target value
|
123
|
+
# @param mapping [Hash] The mapping hash to be updated
|
124
|
+
# @param version [String, nil] The version to create mapping for
|
125
|
+
# @return [Hash] The updated mapping hash
|
126
|
+
def mapping(key_prefix:, value_prefix:, mapping:, version:)
|
127
|
+
if has_item?
|
128
|
+
value_key_prefix = mapping_value_key(value_prefix: value_prefix, collection: true)
|
129
|
+
|
130
|
+
reference_mapping = item.mapping(version:).dup
|
131
|
+
reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}[].#{_1}" }
|
132
|
+
reference_mapping.transform_values! { "#{value_key_prefix}.#{_1}" }
|
133
|
+
|
134
|
+
mapping.merge!(reference_mapping)
|
135
|
+
else
|
136
|
+
properties.values.each do |property|
|
137
|
+
property.mapping(key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping:, version:)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
attr_reader :item, :schema_options, :properties
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
module Properties
|
5
|
+
# Field property type for basic scalar values
|
6
|
+
#
|
7
|
+
# Represents simple scalar types (string, number, integer, boolean) in the schema.
|
8
|
+
# Used for defining basic data fields without nesting.
|
9
|
+
#
|
10
|
+
# @example Define a required string field
|
11
|
+
# field = Verquest::Properties::Field.new(
|
12
|
+
# name: :email,
|
13
|
+
# type: :string,
|
14
|
+
# required: true,
|
15
|
+
# format: "email"
|
16
|
+
# )
|
17
|
+
class Field < Base
|
18
|
+
# List of allowed field types
|
19
|
+
# @return [Array<Symbol>]
|
20
|
+
ALLOWED_TYPES = %i[string number integer boolean].freeze
|
21
|
+
|
22
|
+
# Initialize a new Field property
|
23
|
+
#
|
24
|
+
# @param name [Symbol] The name of the property
|
25
|
+
# @param type [Symbol] The data type for this field, must be one of ALLOWED_TYPES
|
26
|
+
# @param required [Boolean] Whether this property is required
|
27
|
+
# @param map [String, nil] The mapping path for this property
|
28
|
+
# @param schema_options [Hash] Additional JSON schema options for this property
|
29
|
+
# @raise [ArgumentError] If type is not one of the allowed types
|
30
|
+
# @raise [ArgumentError] If attempting to map a field to root without a name
|
31
|
+
def initialize(name:, type:, required: false, map: nil, **schema_options)
|
32
|
+
raise ArgumentError, "Type must be one of #{ALLOWED_TYPES.join(", ")}" unless ALLOWED_TYPES.include?(type)
|
33
|
+
raise ArgumentError, "You can not map fields to the root without a name" if map == "/"
|
34
|
+
|
35
|
+
@name = name
|
36
|
+
@type = type
|
37
|
+
@required = required
|
38
|
+
@map = map
|
39
|
+
@schema_options = schema_options
|
40
|
+
end
|
41
|
+
|
42
|
+
# Generate JSON schema definition for this field
|
43
|
+
#
|
44
|
+
# @return [Hash] The schema definition for this field
|
45
|
+
def to_schema
|
46
|
+
{name => {type: type}.merge(schema_options)}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Create mapping for this field property
|
50
|
+
#
|
51
|
+
# @param key_prefix [Array<Symbol>] Prefix for the source key
|
52
|
+
# @param value_prefix [Array<String>] Prefix for the target value
|
53
|
+
# @param mapping [Hash] The mapping hash to be updated
|
54
|
+
# @param version [String, nil] The version to create mapping for
|
55
|
+
# @return [Hash] The updated mapping hash
|
56
|
+
def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
|
57
|
+
mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :type, :schema_options
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
module Properties
|
5
|
+
# Object property type for structured data
|
6
|
+
#
|
7
|
+
# Represents a complex object with nested properties in the schema.
|
8
|
+
# Used for defining structured data objects with multiple fields.
|
9
|
+
#
|
10
|
+
# @example Define an address object with nested properties
|
11
|
+
# address = Verquest::Properties::Object.new(name: :address)
|
12
|
+
# address.add(Verquest::Properties::Field.new(name: :street, type: :string))
|
13
|
+
# address.add(Verquest::Properties::Field.new(name: :city, type: :string, required: true))
|
14
|
+
class Object < Base
|
15
|
+
# Initialize a new Object property
|
16
|
+
#
|
17
|
+
# @param name [String] The name of the property
|
18
|
+
# @param required [Boolean] Whether this property is required
|
19
|
+
# @param map [String, nil] The mapping path for this property
|
20
|
+
# @param schema_options [Hash] Additional JSON schema options for this property
|
21
|
+
def initialize(name:, required: false, map: nil, **schema_options)
|
22
|
+
@properties = {}
|
23
|
+
|
24
|
+
@name = name
|
25
|
+
@required = required
|
26
|
+
@map = map
|
27
|
+
@schema_options = schema_options
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a child property to this object
|
31
|
+
#
|
32
|
+
# @param property [Verquest::Properties::Base] The property to add to this object
|
33
|
+
# @return [Verquest::Properties::Base] The added property
|
34
|
+
def add(property)
|
35
|
+
properties[property.name] = property
|
36
|
+
end
|
37
|
+
|
38
|
+
# Generate JSON schema definition for this object property
|
39
|
+
#
|
40
|
+
# @return [Hash] The schema definition for this object property
|
41
|
+
def to_schema
|
42
|
+
{
|
43
|
+
name => {
|
44
|
+
type: :object,
|
45
|
+
required: properties.values.select(&:required).map(&:name),
|
46
|
+
properties: properties.transform_values { |property| property.to_schema[property.name] }
|
47
|
+
}.merge(schema_options)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generate validation schema for this object property
|
52
|
+
#
|
53
|
+
# @param version [String, nil] The version to generate validation schema for
|
54
|
+
# @return [Hash] The validation schema for this object property
|
55
|
+
def to_validation_schema(version: nil)
|
56
|
+
{
|
57
|
+
name => {
|
58
|
+
type: :object,
|
59
|
+
required: properties.values.select(&:required).map(&:name),
|
60
|
+
properties: properties.transform_values { |property| property.to_validation_schema(version:)[property.name] }
|
61
|
+
}.merge(schema_options)
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create mapping for this object property and all its children
|
66
|
+
#
|
67
|
+
# @param key_prefix [Array<Symbol>] Prefix for the source key
|
68
|
+
# @param value_prefix [Array<String>] Prefix for the target value
|
69
|
+
# @param mapping [Hash] The mapping hash to be updated
|
70
|
+
# @param version [String, nil] The version to create mapping for
|
71
|
+
# @return [Hash] The updated mapping hash
|
72
|
+
def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
|
73
|
+
properties.values.each do |property|
|
74
|
+
property.mapping(key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), mapping:, version:)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_reader :type, :schema_options, :properties
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
module Properties
|
5
|
+
# Reference property type for schema reuse
|
6
|
+
#
|
7
|
+
# Allows referencing other schema definitions to promote reuse and DRY principles.
|
8
|
+
# Can reference either a complete schema or a specific property within a schema.
|
9
|
+
#
|
10
|
+
# @example Reference another schema
|
11
|
+
# reference = Verquest::Properties::Reference.new(
|
12
|
+
# name: :user,
|
13
|
+
# from: UserRequest,
|
14
|
+
# required: true
|
15
|
+
# )
|
16
|
+
#
|
17
|
+
# @example Reference a specific property from another schema
|
18
|
+
# reference = Verquest::Properties::Reference.new(
|
19
|
+
# name: :address,
|
20
|
+
# from: UserRequest,
|
21
|
+
# property: :address
|
22
|
+
# )
|
23
|
+
class Reference < Base
|
24
|
+
# Initialize a new Reference property
|
25
|
+
#
|
26
|
+
# @param name [String] The name of the property
|
27
|
+
# @param from [Class] The schema class to reference
|
28
|
+
# @param property [Symbol, nil] Optional specific property to reference
|
29
|
+
# @param map [String, nil] The mapping path for this property
|
30
|
+
# @param required [Boolean] Whether this property is required
|
31
|
+
def initialize(name:, from:, property: nil, map: nil, required: false)
|
32
|
+
@name = name
|
33
|
+
@from = from
|
34
|
+
@property = property
|
35
|
+
@map = map
|
36
|
+
@required = required
|
37
|
+
end
|
38
|
+
|
39
|
+
# Generate JSON schema definition for this reference property
|
40
|
+
#
|
41
|
+
# @return [Hash] The schema definition with a $ref pointer
|
42
|
+
def to_schema
|
43
|
+
{
|
44
|
+
name => {"$ref": from.to_ref(property:)}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Generate validation schema for this reference property
|
49
|
+
#
|
50
|
+
# @param version [String, nil] The version to generate validation schema for
|
51
|
+
# @return [Hash] The validation schema for this reference
|
52
|
+
def to_validation_schema(version: nil)
|
53
|
+
{
|
54
|
+
name => from.to_validation_schema(version:, property: property)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create mapping for this reference property
|
59
|
+
# This delegates to the referenced schema's mapping with appropriate key prefixing
|
60
|
+
#
|
61
|
+
# @param key_prefix [Array<Symbol>] Prefix for the source key
|
62
|
+
# @param value_prefix [Array<String>] Prefix for the target value
|
63
|
+
# @param mapping [Hash] The mapping hash to be updated
|
64
|
+
# @param version [String, nil] The version to create mapping for
|
65
|
+
# @return [Hash] The updated mapping hash
|
66
|
+
def mapping(key_prefix:, value_prefix:, mapping:, version:)
|
67
|
+
reference_mapping = from.mapping(version:, property:).dup
|
68
|
+
value_key_prefix = mapping_value_key(value_prefix:)
|
69
|
+
|
70
|
+
# Single field mapping
|
71
|
+
if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?(".")
|
72
|
+
reference_mapping = {
|
73
|
+
(key_prefix + [name]).join(".") => value_key_prefix
|
74
|
+
}
|
75
|
+
else
|
76
|
+
if value_key_prefix != "" && !value_key_prefix.end_with?(".")
|
77
|
+
value_key_prefix = "#{value_key_prefix}."
|
78
|
+
end
|
79
|
+
|
80
|
+
reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}.#{_1}" }
|
81
|
+
reference_mapping.transform_values! { "#{value_key_prefix}#{_1}" }
|
82
|
+
end
|
83
|
+
|
84
|
+
mapping.merge!(reference_mapping)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_reader :from, :property
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
# Property types for defining versioned API request schemas
|
5
|
+
#
|
6
|
+
# The Properties module contains classes representing different types of
|
7
|
+
# properties that can be used when defining API request schemas. Each property
|
8
|
+
# type knows how to generate its own schema representation and handles mapping
|
9
|
+
# between external and internal parameter structures.
|
10
|
+
#
|
11
|
+
# @example Using properties in a schema definition
|
12
|
+
# class UserRequest < Verquest::Base
|
13
|
+
# version "2023-01" do
|
14
|
+
# # Field - Basic scalar properties
|
15
|
+
# field :email, type: :string, required: true
|
16
|
+
#
|
17
|
+
# # Object - Nested structure with properties
|
18
|
+
# object :address do
|
19
|
+
# field :street, type: :string
|
20
|
+
# field :city, type: :string, required: true
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # Collection - Array of objects
|
24
|
+
# collection :orders do
|
25
|
+
# field :id, type: :string, required: true
|
26
|
+
# field :amount, type: :number
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # Array - Simple array of scalar values
|
30
|
+
# array :tags, type: :string
|
31
|
+
#
|
32
|
+
# # Reference - Reference to another schema
|
33
|
+
# reference :payment, from: PaymentRequest
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @see Verquest::Properties::Base Base class for all property types
|
38
|
+
# @see Verquest::Properties::Field For scalar values like strings and numbers
|
39
|
+
# @see Verquest::Properties::Object For nested objects with their own properties
|
40
|
+
# @see Verquest::Properties::Collection For arrays of structured objects
|
41
|
+
# @see Verquest::Properties::Array For arrays of scalar values
|
42
|
+
# @see Verquest::Properties::Reference For references to other schemas
|
43
|
+
module Properties
|
44
|
+
# This module is a namespace for property type classes
|
45
|
+
# Each property type is defined in its own file under lib/verquest/properties/
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verquest
|
4
|
+
# A result object for operation outcomes
|
5
|
+
#
|
6
|
+
# Result represents the outcome of an operation in the Verquest gem,
|
7
|
+
# particularly parameter mapping and validation operations. It follows
|
8
|
+
# the Result pattern, providing a consistent interface for both successful
|
9
|
+
# and failed operations, avoiding exceptions for control flow.
|
10
|
+
#
|
11
|
+
# @example Handling a successful result
|
12
|
+
# result = Verquest::Result.success(transformed_params)
|
13
|
+
# if result.success?
|
14
|
+
# process_params(result.value)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Handling a failed result
|
18
|
+
# result = Verquest::Result.failure(["Invalid email format"])
|
19
|
+
# if result.failure?
|
20
|
+
# display_errors(result.errors)
|
21
|
+
# end
|
22
|
+
class Result
|
23
|
+
# @!attribute [r] success
|
24
|
+
# @return [Boolean] Whether the operation was successful
|
25
|
+
#
|
26
|
+
# @!attribute [r] value
|
27
|
+
# @return [Object, nil] The result value if successful, nil otherwise
|
28
|
+
#
|
29
|
+
# @!attribute [r] errors
|
30
|
+
# @return [Array] List of errors if failed, empty array otherwise
|
31
|
+
attr_reader :success, :value, :errors
|
32
|
+
|
33
|
+
# Initialize a new Result instance
|
34
|
+
#
|
35
|
+
# @param success [Boolean] Whether the operation was successful
|
36
|
+
# @param value [Object, nil] The result value for successful operations
|
37
|
+
# @param errors [Array, nil] List of errors for failed operations
|
38
|
+
# @return [Result] A new Result instance
|
39
|
+
def initialize(success:, value: nil, errors: nil)
|
40
|
+
@success = success
|
41
|
+
@value = value
|
42
|
+
@errors = errors
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create a successful result with a value
|
46
|
+
#
|
47
|
+
# @param value [Object] The successful operation's result value
|
48
|
+
# @return [Result] A successful Result instance containing the value
|
49
|
+
def self.success(value)
|
50
|
+
new(success: true, value: value)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a failed result with errors
|
54
|
+
#
|
55
|
+
# @param errors [Array, String] Error message(s) describing the failure
|
56
|
+
# @return [Result] A failed Result instance containing the errors
|
57
|
+
def self.failure(errors)
|
58
|
+
new(success: false, errors: errors)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the result represents a successful operation
|
62
|
+
#
|
63
|
+
# @return [Boolean] true if the operation was successful, false otherwise
|
64
|
+
def success?
|
65
|
+
success
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check if the result represents a failed operation
|
69
|
+
#
|
70
|
+
# @return [Boolean] true if the operation failed, false otherwise
|
71
|
+
def failure?
|
72
|
+
!success
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Verquest
|
2
|
+
# Transforms parameters based on path mappings
|
3
|
+
#
|
4
|
+
# The Transformer class handles the conversion of parameter structures based on
|
5
|
+
# a mapping of source paths to target paths. It supports deep nested structures,
|
6
|
+
# array notations, and complex path expressions using dot notation.
|
7
|
+
#
|
8
|
+
# @example Basic transformation
|
9
|
+
# mapping = {
|
10
|
+
# "user.firstName" => "user.first_name",
|
11
|
+
# "user.lastName" => "user.last_name",
|
12
|
+
# "addresses[].zip" => "addresses[].postal_code"
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# transformer = Verquest::Transformer.new(mapping: mapping)
|
16
|
+
# result = transformer.call({
|
17
|
+
# user: {
|
18
|
+
# firstName: "John",
|
19
|
+
# lastName: "Doe"
|
20
|
+
# },
|
21
|
+
# addresses: [
|
22
|
+
# { zip: "12345" },
|
23
|
+
# { zip: "67890" }
|
24
|
+
# ]
|
25
|
+
# })
|
26
|
+
#
|
27
|
+
# # Result will be:
|
28
|
+
# # {
|
29
|
+
# # user: {
|
30
|
+
# # first_name: "John",
|
31
|
+
# # last_name: "Doe"
|
32
|
+
# # },
|
33
|
+
# # addresses: [
|
34
|
+
# # { postal_code: "12345" },
|
35
|
+
# # { postal_code: "67890" }
|
36
|
+
# # ]
|
37
|
+
# # }
|
38
|
+
class Transformer
|
39
|
+
# Creates a new Transformer with the specified mapping
|
40
|
+
#
|
41
|
+
# @param mapping [Hash] A hash where keys are source paths and values are target paths
|
42
|
+
# @return [Transformer] A new transformer instance
|
43
|
+
def initialize(mapping:)
|
44
|
+
@mapping = mapping
|
45
|
+
@path_cache = {} # Cache for parsed paths to improve performance
|
46
|
+
precompile_paths # Prepare cache during initialization
|
47
|
+
end
|
48
|
+
|
49
|
+
# Transforms input parameters according to the provided mapping
|
50
|
+
#
|
51
|
+
# @param params [Hash] The input parameters to transform
|
52
|
+
# @return [Hash] The transformed parameters with symbol keys
|
53
|
+
def call(params)
|
54
|
+
result = {}
|
55
|
+
|
56
|
+
mapping.each do |source_path, target_path|
|
57
|
+
# Extract value using the source path
|
58
|
+
value = extract_value(params, parse_path(source_path.to_s))
|
59
|
+
next if value.nil?
|
60
|
+
|
61
|
+
# Set the extracted value at the target path
|
62
|
+
set_value(result, parse_path(target_path.to_s), value)
|
63
|
+
end
|
64
|
+
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# @!attribute [r] mapping
|
71
|
+
# @return [Hash] The source-to-target path mapping
|
72
|
+
# @!attribute [r] path_cache
|
73
|
+
# @return [Hash] Cache for parsed paths
|
74
|
+
attr_reader :mapping, :path_cache
|
75
|
+
|
76
|
+
# Precompiles all paths from the mapping to improve performance
|
77
|
+
# This is called during initialization to prepare the cache
|
78
|
+
#
|
79
|
+
# @return [void]
|
80
|
+
def precompile_paths
|
81
|
+
mapping.each do |source_path, target_path|
|
82
|
+
parse_path(source_path.to_s)
|
83
|
+
parse_path(target_path.to_s)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Parses a dot-notation path into structured path parts
|
88
|
+
# Uses memoization for performance optimization
|
89
|
+
#
|
90
|
+
# @param path [String] The dot-notation path (e.g., "user.address.street")
|
91
|
+
# @return [Array<Hash>] Array of path parts with :key and :array attributes
|
92
|
+
def parse_path(path)
|
93
|
+
path_cache[path] ||= path.split(".").map do |part|
|
94
|
+
if part.end_with?("[]")
|
95
|
+
{key: part[0...-2], array: true}
|
96
|
+
else
|
97
|
+
{key: part, array: false}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extracts a value from nested data structure using the parsed path parts
|
103
|
+
#
|
104
|
+
# @param data [Hash, Array, Object] The data to extract value from
|
105
|
+
# @param path_parts [Array<Hash>] The parsed path parts
|
106
|
+
# @return [Object, nil] The extracted value or nil if not found
|
107
|
+
def extract_value(data, path_parts)
|
108
|
+
return data if path_parts.empty?
|
109
|
+
|
110
|
+
current_part = path_parts.first
|
111
|
+
remaining_path = path_parts[1..]
|
112
|
+
key = current_part[:key]
|
113
|
+
|
114
|
+
case data
|
115
|
+
when Hash
|
116
|
+
# Check if the key exists (as string or symbol)
|
117
|
+
return nil unless data.key?(key.to_s) || data.key?(key.to_sym)
|
118
|
+
|
119
|
+
# Determine the actual key type used in the hash
|
120
|
+
actual_key = data.key?(key.to_s) ? key.to_s : key.to_sym
|
121
|
+
value = data[actual_key]
|
122
|
+
|
123
|
+
if current_part[:array] && value.is_a?(Array)
|
124
|
+
# Process array elements and filter out nil values
|
125
|
+
value.map { |item| extract_value(item, remaining_path) }.compact
|
126
|
+
else
|
127
|
+
# Continue traversing the path
|
128
|
+
extract_value(value, remaining_path)
|
129
|
+
end
|
130
|
+
when Array
|
131
|
+
if current_part[:array]
|
132
|
+
# Map through array elements with remaining path
|
133
|
+
data.map { |item| extract_value(item, remaining_path) }.compact
|
134
|
+
else
|
135
|
+
# Try to extract from each array element with the full path
|
136
|
+
result = data.map { |item| extract_value(item, path_parts) }.compact
|
137
|
+
result.empty? ? nil : result
|
138
|
+
end
|
139
|
+
else
|
140
|
+
# For scalar values, return only if we're at the end of the path
|
141
|
+
remaining_path.empty? ? data : nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Sets a value in a result hash at the specified path
|
146
|
+
#
|
147
|
+
# @param result [Hash] The result hash to modify
|
148
|
+
# @param path_parts [Array<Hash>] The parsed path parts
|
149
|
+
# @param value [Object] The value to set
|
150
|
+
# @return [Hash] The modified result hash with symbol keys
|
151
|
+
def set_value(result, path_parts, value)
|
152
|
+
return result if path_parts.empty?
|
153
|
+
|
154
|
+
current_part = path_parts.first
|
155
|
+
remaining_path = path_parts[1..]
|
156
|
+
key = current_part[:key].to_sym # Convert key to symbol for consistent symbol keys
|
157
|
+
|
158
|
+
if remaining_path.empty?
|
159
|
+
# End of path, set the value directly
|
160
|
+
result[key] = value
|
161
|
+
elsif current_part[:array] && value.is_a?(Array)
|
162
|
+
# Handle array notation in target path
|
163
|
+
result[key] ||= []
|
164
|
+
|
165
|
+
# Process each value in the array
|
166
|
+
value.each_with_index do |v, i|
|
167
|
+
result[key][i] ||= {}
|
168
|
+
set_value(result[key][i], remaining_path, v)
|
169
|
+
end
|
170
|
+
else
|
171
|
+
# Continue building nested structure
|
172
|
+
result[key] ||= {}
|
173
|
+
set_value(result[key], remaining_path, value)
|
174
|
+
end
|
175
|
+
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|