schemable 0.1.3 → 1.0.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/Gemfile +5 -5
- data/Gemfile.lock +82 -50
- data/README.md +324 -100
- data/lib/generators/schemable/install_generator.rb +3 -13
- data/lib/generators/schemable/model_generator.rb +1 -16
- data/lib/schemable/attribute_schema_generator.rb +130 -0
- data/lib/schemable/configuration.rb +114 -0
- data/lib/schemable/definition.rb +322 -0
- data/lib/schemable/included_schema_generator.rb +107 -0
- data/lib/schemable/relationship_schema_generator.rb +119 -0
- data/lib/schemable/request_schema_generator.rb +88 -0
- data/lib/schemable/response_schema_generator.rb +124 -0
- data/lib/schemable/schema_modifier.rb +248 -0
- data/lib/schemable/version.rb +1 -1
- data/lib/schemable.rb +60 -927
- data/lib/templates/schemable.rb +76 -0
- data/schemable.gemspec +2 -5
- data/sig/schemable/attribute_schema_generator.rbs +13 -0
- data/sig/schemable/configuration.rbs +21 -0
- data/sig/schemable/definition.rbs +32 -0
- data/sig/schemable/included_schema_generator.rbs +11 -0
- data/sig/schemable/relationship_schema_generator.rbs +11 -0
- data/sig/schemable/request_schema_generator.rbs +10 -0
- data/sig/schemable/response_schema_generator.rbs +13 -0
- data/sig/schemable/schema_modifier.rbs +9 -0
- data/sig/schemable.rbs +4 -1
- metadata +23 -36
- data/lib/templates/common_definitions.rb +0 -13
- data/lib/templates/serializers_helper.rb +0 -7
@@ -0,0 +1,119 @@
|
|
1
|
+
module Schemable
|
2
|
+
# The RelationshipSchemaGenerator class is responsible for generating the 'relationships' part of a JSON:API compliant response.
|
3
|
+
# This class generates schemas for each relationship of a model, including 'belongs_to' (and has_many) and 'has_many' relationships.
|
4
|
+
#
|
5
|
+
# @see Schemable
|
6
|
+
class RelationshipSchemaGenerator
|
7
|
+
attr_reader :model_definition, :schema_modifier, :relationships
|
8
|
+
|
9
|
+
# Initializes a new RelationshipSchemaGenerator instance.
|
10
|
+
#
|
11
|
+
# @param model_definition [ModelDefinition] The model definition to generate the schema for.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# generator = RelationshipSchemaGenerator.new(model_definition)
|
15
|
+
def initialize(model_definition)
|
16
|
+
@model_definition = model_definition
|
17
|
+
@schema_modifier = SchemaModifier.new
|
18
|
+
@relationships = model_definition.relationships
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generates the 'relationships' part of the JSON:API response.
|
22
|
+
# It iterates over each relationship type (belongs_to, has_many) and for each relationship,
|
23
|
+
# it prepares a schema unless the relationship is excluded from expansion.
|
24
|
+
# If the 'expand' option is true, it changes the schema to include type and id properties inside the 'meta' property.
|
25
|
+
#
|
26
|
+
# @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from expansion.
|
27
|
+
# @param expand [Boolean] Whether to include the relationships of the related resource in the schema.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: [:some_relationship])
|
31
|
+
#
|
32
|
+
# @return [Hash] The generated schema.
|
33
|
+
def generate(relationships_to_exclude_from_expansion: [], expand: false)
|
34
|
+
return {} if @relationships.blank? || @relationships == { belongs_to: {}, has_many: {} }
|
35
|
+
|
36
|
+
schema = {
|
37
|
+
type: :object,
|
38
|
+
properties: {}
|
39
|
+
}
|
40
|
+
|
41
|
+
%i[belongs_to has_many].each do |relation_type|
|
42
|
+
@relationships[relation_type]&.each do |relation, definition|
|
43
|
+
non_expanded_data_properties = {
|
44
|
+
type: :object,
|
45
|
+
properties: {
|
46
|
+
meta: {
|
47
|
+
type: :object,
|
48
|
+
properties: {
|
49
|
+
included: { type: :boolean, default: false }
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
result = relation_type == :belongs_to ? generate_schema(definition.model_name) : generate_schema(definition.model_name, collection: true)
|
56
|
+
|
57
|
+
result = non_expanded_data_properties if !expand || relationships_to_exclude_from_expansion.include?(definition.model_name)
|
58
|
+
|
59
|
+
schema[:properties].merge!(relation => result)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Modify the schema to include additional response relations
|
64
|
+
schema = @schema_modifier.add_properties(schema, @model_definition.additional_response_relations, 'properties')
|
65
|
+
|
66
|
+
# Modify the schema to exclude response relations
|
67
|
+
@model_definition.excluded_response_relations.each do |key|
|
68
|
+
schema = @schema_modifier.delete_properties(schema, "properties.#{key}")
|
69
|
+
end
|
70
|
+
|
71
|
+
schema
|
72
|
+
end
|
73
|
+
|
74
|
+
# Generates the schema for a specific relationship.
|
75
|
+
# If the 'collection' option is true, it generates a schema for a 'has_many' relationship,
|
76
|
+
# otherwise it generates a schema for a 'belongs_to' relationship. The difference between the two is that
|
77
|
+
# 'data' is an array in the 'has_many' relationship and an object in the 'belongs_to' relationship.
|
78
|
+
#
|
79
|
+
# @param type_name [String] The type of the related resource.
|
80
|
+
# @param collection [Boolean] Whether the relationship is a 'has_many' relationship.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# relationship_schema = generator.generate_schema('resource_type', collection: true)
|
84
|
+
#
|
85
|
+
# @return [Hash] The generated schema for the relationship.
|
86
|
+
def generate_schema(type_name, collection: false)
|
87
|
+
if collection
|
88
|
+
{
|
89
|
+
type: :object,
|
90
|
+
properties: {
|
91
|
+
data: {
|
92
|
+
type: :array,
|
93
|
+
items: {
|
94
|
+
type: :object,
|
95
|
+
properties: {
|
96
|
+
id: { type: :string },
|
97
|
+
type: { type: :string, default: type_name }
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
else
|
104
|
+
{
|
105
|
+
type: :object,
|
106
|
+
properties: {
|
107
|
+
data: {
|
108
|
+
type: :object,
|
109
|
+
properties: {
|
110
|
+
id: { type: :string },
|
111
|
+
type: { type: :string, default: type_name }
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Schemable
|
2
|
+
# The RequestSchemaGenerator class is responsible for generating JSON schemas for create and update requests.
|
3
|
+
# This class generates schemas based on the model definition, including additional and excluded attributes.
|
4
|
+
#
|
5
|
+
# @see Schemable
|
6
|
+
class RequestSchemaGenerator
|
7
|
+
attr_reader :model_definition, :schema_modifier
|
8
|
+
|
9
|
+
# Initializes a new RequestSchemaGenerator instance.
|
10
|
+
#
|
11
|
+
# @param model_definition [ModelDefinition] The model definition to generate the schema for.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# generator = RequestSchemaGenerator.new(model_definition)
|
15
|
+
def initialize(model_definition)
|
16
|
+
@model_definition = model_definition
|
17
|
+
@schema_modifier = SchemaModifier.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates the JSON schema for a create request.
|
21
|
+
# It generates a schema for the model attributes and then modifies it based on the additional and excluded attributes for create requests.
|
22
|
+
# It also determines the required attributes based on the optional and nullable attributes.
|
23
|
+
# Note that it is presumed that the model is using the same fields/columns for create as well as responses.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# schema = generator.generate_for_create
|
27
|
+
#
|
28
|
+
# @return [Hash] The generated schema for create requests.
|
29
|
+
def generate_for_create
|
30
|
+
schema = {
|
31
|
+
type: :object,
|
32
|
+
properties: {
|
33
|
+
data: AttributeSchemaGenerator.new(@model_definition).generate
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
@schema_modifier.add_properties(schema, @model_definition.additional_create_request_attributes, 'properties.data.properties')
|
38
|
+
|
39
|
+
@model_definition.excluded_create_request_attributes.each do |key|
|
40
|
+
@schema_modifier.delete_properties(schema, "properties.data.properties.#{key}")
|
41
|
+
end
|
42
|
+
|
43
|
+
required_attributes = {
|
44
|
+
required: (
|
45
|
+
schema.as_json['properties']['data']['properties'].keys -
|
46
|
+
@model_definition.optional_create_request_attributes.map(&:to_s) -
|
47
|
+
@model_definition.nullable_attributes.map(&:to_s)
|
48
|
+
).map { |key| key.to_s.camelize(:lower).to_sym }
|
49
|
+
}
|
50
|
+
|
51
|
+
@schema_modifier.add_properties(schema, required_attributes, 'properties.data')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generates the JSON schema for a update request.
|
55
|
+
# It generates a schema for the model attributes and then modifies it based on the additional and excluded attributes for update requests.
|
56
|
+
# It also determines the required attributes based on the optional and nullable attributes.
|
57
|
+
# Note that it is presumed that the model is using the same fields/columns for update as well as responses.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# schema = generator.generate_for_update
|
61
|
+
#
|
62
|
+
# @return [Hash] The generated schema for update requests.
|
63
|
+
def generate_for_update
|
64
|
+
schema = {
|
65
|
+
type: :object,
|
66
|
+
properties: {
|
67
|
+
data: AttributeSchemaGenerator.new(@model_definition).generate
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
@schema_modifier.add_properties(schema, @model_definition.additional_update_request_attributes, 'properties.data.properties')
|
72
|
+
|
73
|
+
@model_definition.excluded_update_request_attributes.each do |key|
|
74
|
+
@schema_modifier.delete_properties(schema, "properties.data.properties.#{key}")
|
75
|
+
end
|
76
|
+
|
77
|
+
required_attributes = {
|
78
|
+
required: (
|
79
|
+
schema.as_json['properties']['data']['properties'].keys -
|
80
|
+
@model_definition.optional_update_request_attributes.map(&:to_s) -
|
81
|
+
@model_definition.nullable_attributes.map(&:to_s)
|
82
|
+
).map { |key| key.to_s.camelize(:lower).to_sym }
|
83
|
+
}
|
84
|
+
|
85
|
+
@schema_modifier.add_properties(schema, required_attributes, 'properties.data')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Schemable
|
2
|
+
# The ResponseSchemaGenerator class is responsible for generating JSON schemas for responses.
|
3
|
+
# This class generates schemas based on the model definition, including attributes, relationships, and included resources.
|
4
|
+
#
|
5
|
+
# @see Schemable
|
6
|
+
class ResponseSchemaGenerator
|
7
|
+
attr_reader :model_definition, :model, :schema_modifier, :configuration
|
8
|
+
|
9
|
+
# Initializes a new ResponseSchemaGenerator instance.
|
10
|
+
#
|
11
|
+
# @param model_definition [ModelDefinition] The model definition to generate the schema for.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# generator = ResponseSchemaGenerator.new(model_definition)
|
15
|
+
def initialize(model_definition)
|
16
|
+
@model_definition = model_definition
|
17
|
+
@model = model_definition.model
|
18
|
+
@schema_modifier = SchemaModifier.new
|
19
|
+
@configuration = Schemable.configuration
|
20
|
+
end
|
21
|
+
|
22
|
+
# Generates the JSON schema for a response.
|
23
|
+
# It generates a schema for the model attributes and relationships, and if the 'expand' option is true,
|
24
|
+
# it also includes the included resources in the schema.
|
25
|
+
# It also adds meta and jsonapi information to the schema.
|
26
|
+
#
|
27
|
+
# @param expand [Boolean] Whether to include the included resources in the schema.
|
28
|
+
# @param relationships_to_exclude_from_expansion [Array] The relationships to exclude from expansion in the schema.
|
29
|
+
# @param collection [Boolean] Whether the response is for a collection of resources.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# schema = generator.generate(expand: true, relationships_to_exclude_from_expansion: ['some_relationship'], collection: true)
|
33
|
+
#
|
34
|
+
# @return [Hash] The generated schema.
|
35
|
+
def generate(expand: false, relationships_to_exclude_from_expansion: [], collection: false)
|
36
|
+
data = {
|
37
|
+
type: :object,
|
38
|
+
properties: {
|
39
|
+
type: { type: :string, default: @model_definition.model_name },
|
40
|
+
id: { type: :string },
|
41
|
+
attributes: AttributeSchemaGenerator.new(@model_definition).generate
|
42
|
+
}.merge(
|
43
|
+
if @model_definition.relationships.blank? || @model_definition.relationships == { belongs_to: {}, has_many: {} }
|
44
|
+
{}
|
45
|
+
else
|
46
|
+
{ relationships: RelationshipSchemaGenerator.new(@model_definition).generate(expand:, relationships_to_exclude_from_expansion:) }
|
47
|
+
end
|
48
|
+
)
|
49
|
+
}
|
50
|
+
|
51
|
+
schema = collection ? { data: { type: :array, items: data } } : { data: }
|
52
|
+
|
53
|
+
if expand
|
54
|
+
included_schema = IncludedSchemaGenerator.new(@model_definition).generate(expand:, relationships_to_exclude_from_expansion:)
|
55
|
+
@schema_modifier.add_properties(schema, included_schema, '.')
|
56
|
+
end
|
57
|
+
|
58
|
+
@schema_modifier.add_properties(schema, { meta: }, '.') if collection
|
59
|
+
@schema_modifier.add_properties(schema, { jsonapi: }, '.')
|
60
|
+
|
61
|
+
{ type: :object, properties: schema }.compact_blank
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generates the JSON schema for the 'meta' part of a response.
|
65
|
+
# It returns a custom meta response schema if one is defined in the configuration, otherwise it generates a default meta schema.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# meta_schema = generator.meta
|
69
|
+
#
|
70
|
+
# @return [Hash] The generated schema for the 'meta' part of a response.
|
71
|
+
def meta
|
72
|
+
return @configuration.custom_meta_response_schema if @configuration.custom_meta_response_schema.present?
|
73
|
+
|
74
|
+
if @configuration.pagination_enabled
|
75
|
+
{
|
76
|
+
type: :object,
|
77
|
+
properties: {
|
78
|
+
page: {
|
79
|
+
type: :object,
|
80
|
+
properties: {
|
81
|
+
totalPages: {
|
82
|
+
type: :integer,
|
83
|
+
default: 1
|
84
|
+
},
|
85
|
+
count: {
|
86
|
+
type: :integer,
|
87
|
+
default: 1
|
88
|
+
},
|
89
|
+
rowsPerPage: {
|
90
|
+
type: :integer,
|
91
|
+
default: 1
|
92
|
+
},
|
93
|
+
currentPage: {
|
94
|
+
type: :integer,
|
95
|
+
default: 1
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
else
|
102
|
+
{}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Generates the JSON schema for the 'jsonapi' part of a response.
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# jsonapi_schema = generator.jsonapi
|
110
|
+
#
|
111
|
+
# @return [Hash] The generated schema for the 'jsonapi' part of a response.
|
112
|
+
def jsonapi
|
113
|
+
{
|
114
|
+
type: :object,
|
115
|
+
properties: {
|
116
|
+
version: {
|
117
|
+
type: :string,
|
118
|
+
default: '1.0'
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module Schemable
|
2
|
+
# The SchemaModifier class provides methods for modifying a given schema.
|
3
|
+
# It includes methods for parsing paths, checking if a path exists in a schema,
|
4
|
+
# deeply merging hashes, adding properties to a schema, and deleting properties from a schema.
|
5
|
+
#
|
6
|
+
# @see Schemable
|
7
|
+
class SchemaModifier
|
8
|
+
|
9
|
+
# Parses a given path into an array of symbols.
|
10
|
+
#
|
11
|
+
# @note This method accepts paths in the following formats:
|
12
|
+
# - 'path.to.property'
|
13
|
+
# - 'path.to.array.[0].property'
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# parse_path('path.to.property') #=> [:path, :to, :property]
|
17
|
+
# parse_path('path.to.array.[0].property') #=> [:path, :to, :array, :[0], :property]
|
18
|
+
#
|
19
|
+
# @param path [String] The path to parse.
|
20
|
+
# @return [Array<Symbol>] The parsed path.
|
21
|
+
def parse_path(path)
|
22
|
+
path.split('.').map(&:to_sym)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Checks if a given path exists in a schema.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# schema = {
|
29
|
+
# path: {
|
30
|
+
# type: :object,
|
31
|
+
# properties: {
|
32
|
+
# to: {
|
33
|
+
# type: :object,
|
34
|
+
# properties: {
|
35
|
+
# property: {
|
36
|
+
# type: :string
|
37
|
+
# }
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
# }
|
41
|
+
# }
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# path = 'path.properties.to.properties.property'
|
45
|
+
# incorrect_path = 'path.properties.to.properties.invalid'
|
46
|
+
# path_exists?(schema, path) #=> true
|
47
|
+
# path_exists?(schema, incorrect_path) #=> false
|
48
|
+
#
|
49
|
+
# @param schema [Hash, Array] The schema to check.
|
50
|
+
# @param path [String] The path to check for.
|
51
|
+
# @return [Boolean] True if the path exists in the schema, false otherwise.
|
52
|
+
def path_exists?(schema, path)
|
53
|
+
path_segments = parse_path(path)
|
54
|
+
|
55
|
+
path_segments.reduce(schema) do |current_segment, next_segment|
|
56
|
+
if current_segment.is_a?(Array)
|
57
|
+
# The regex pattern '/\[(\d+)\]|\d+/' matches square brackets containing one or more digits,
|
58
|
+
# or standalone digits. Used for parsing array indices in a path.
|
59
|
+
index = next_segment.to_s.match(/\[(\d+)\]|\d+/)[1]
|
60
|
+
# The regex pattern '/\A\d+\z/' matches a sequence of one or more digits from the start ('\A')
|
61
|
+
# to the end ('\z') of a string. It checks if a string consists of only digits.
|
62
|
+
return false if index.nil? || !index.match?(/\A\d+\z/) || index.to_i >= current_segment.length
|
63
|
+
|
64
|
+
current_segment[index.to_i]
|
65
|
+
else
|
66
|
+
return false unless current_segment.is_a?(Hash) && current_segment.key?(next_segment)
|
67
|
+
|
68
|
+
current_segment[next_segment]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
# Deeply merges two hashes.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# destination = { level1: { level2: { level3: 'value' } } }
|
79
|
+
# new_data = { level1_again: 'value' }
|
80
|
+
# deep_merge_hashes(destination, new_data)
|
81
|
+
# #=> { level1: { level2: { level3: 'value' } }, level1_again: 'value' }
|
82
|
+
#
|
83
|
+
# new_destination = [{ object1: 'value' }, { object2: 'value' }]
|
84
|
+
# new_new_data = { object3: 'value' }
|
85
|
+
# deep_merge_hashes(new_destination, new_new_data)
|
86
|
+
# #=> [{ object1: 'value' }, { object2: 'value' }, { object3: 'value' }]
|
87
|
+
#
|
88
|
+
# new_destination = { object1: 'value' }
|
89
|
+
# new_new_data = [{ object2: 'value' }, { object3: 'value' }]
|
90
|
+
# deep_merge_hashes(new_destination, new_new_data)
|
91
|
+
# #=> { object1: 'value', object2: 'value', object3: 'value' }
|
92
|
+
#
|
93
|
+
# @param destination [Hash] The hash to merge into.
|
94
|
+
# @param new_data [Hash] The hash to merge from.
|
95
|
+
# @return [Hash] The merged hashes.
|
96
|
+
def deep_merge_hashes(destination, new_data)
|
97
|
+
if destination.is_a?(Hash) && new_data.is_a?(Array)
|
98
|
+
destination.merge(new_data)
|
99
|
+
elsif destination.is_a?(Array) && new_data.is_a?(Hash)
|
100
|
+
destination.push(new_data)
|
101
|
+
elsif destination.is_a?(Hash) && new_data.is_a?(Hash)
|
102
|
+
new_data.each do |key, value|
|
103
|
+
if destination[key].is_a?(Hash) && value.is_a?(Hash)
|
104
|
+
destination[key] = deep_merge_hashes(destination[key], value)
|
105
|
+
elsif destination[key].is_a?(Array) && value.is_a?(Array)
|
106
|
+
destination[key].concat(value)
|
107
|
+
elsif destination[key].is_a?(Array) && value.is_a?(Hash)
|
108
|
+
destination[key].push(value)
|
109
|
+
else
|
110
|
+
destination[key] = value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
destination
|
116
|
+
end
|
117
|
+
|
118
|
+
# Adds properties to a schema at a given path.
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# original_schema = { level1: { level2: { level3: 'value' } } }
|
122
|
+
# new_data = { L3: 'value' }
|
123
|
+
# path = 'level1.level2'
|
124
|
+
# add_properties(original_schema, new_schema, path)
|
125
|
+
# #=> { level1: { level2: { level3: 'value', L3: 'value' } } }
|
126
|
+
#
|
127
|
+
# new_original_schema = { test: [{ object1: 'value' }, { object2: 'value' }] }
|
128
|
+
# new_new_schema = { object2_again: 'value' }
|
129
|
+
# path = 'test.[1]'
|
130
|
+
# add_properties(new_original_schema, new_new_schema, path)
|
131
|
+
# #=> { test: [{ object1: 'value' }, { object2: 'value', object2_again: 'value' }] }
|
132
|
+
#
|
133
|
+
# @param original_schema [Hash] The original schema.
|
134
|
+
# @param new_schema [Hash] The new schema to add.
|
135
|
+
# @param path [String] The path at which to add the new schema.
|
136
|
+
# @note This method accepts paths in the following formats:
|
137
|
+
# - 'path.to.property'
|
138
|
+
# - 'path.to.array.[0].property'
|
139
|
+
# - '.'
|
140
|
+
#
|
141
|
+
# @return [Hash] The modified schema.
|
142
|
+
def add_properties(original_schema, new_schema, path)
|
143
|
+
return deep_merge_hashes(original_schema, new_schema) if path == '.'
|
144
|
+
|
145
|
+
unless path_exists?(original_schema, path)
|
146
|
+
puts "Error: Path '#{path}' does not exist in the original schema"
|
147
|
+
return original_schema
|
148
|
+
end
|
149
|
+
|
150
|
+
path_segments = parse_path(path)
|
151
|
+
current_segment = original_schema
|
152
|
+
last_segment = path_segments.pop
|
153
|
+
|
154
|
+
# Navigate to the specified location in the schema
|
155
|
+
path_segments.each do |segment|
|
156
|
+
if current_segment.is_a?(Array)
|
157
|
+
index = segment.to_s.match(/\[(\d+)\]|\d+/)[1]
|
158
|
+
if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length
|
159
|
+
current_segment = current_segment[index.to_i]
|
160
|
+
else
|
161
|
+
puts "Error: Invalid index in path '#{path}'"
|
162
|
+
return original_schema
|
163
|
+
end
|
164
|
+
elsif current_segment.is_a?(Hash) && current_segment.key?(segment)
|
165
|
+
current_segment = current_segment[segment]
|
166
|
+
else
|
167
|
+
puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'"
|
168
|
+
return original_schema
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Merge the new schema into the specified location
|
173
|
+
if current_segment.is_a?(Array)
|
174
|
+
index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1]
|
175
|
+
if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length
|
176
|
+
current_segment[index.to_i] = deep_merge_hashes(current_segment[index.to_i], new_schema)
|
177
|
+
else
|
178
|
+
puts "Error: Invalid index in path '#{path}'"
|
179
|
+
end
|
180
|
+
else
|
181
|
+
current_segment[last_segment] = deep_merge_hashes(current_segment[last_segment], new_schema)
|
182
|
+
end
|
183
|
+
|
184
|
+
original_schema
|
185
|
+
end
|
186
|
+
|
187
|
+
# Deletes properties from a schema at a given path.
|
188
|
+
#
|
189
|
+
# @example
|
190
|
+
# original_schema = { level1: { level2: { level3: 'value' } } }
|
191
|
+
# path = 'level1.level2'
|
192
|
+
# delete_properties(original_schema, path)
|
193
|
+
# #=> { level1: {} }
|
194
|
+
#
|
195
|
+
# new_original_schema = { test: [{ object1: 'value' }, { object2: 'value' }] }
|
196
|
+
# path = 'test.[1]'
|
197
|
+
# delete_properties(new_original_schema, path)
|
198
|
+
# #=> { test: [{ object1: 'value' }] }
|
199
|
+
#
|
200
|
+
# @param original_schema [Hash] The original schema.
|
201
|
+
# @param path [String] The path at which to delete properties.
|
202
|
+
# @return [Hash] The modified schema.
|
203
|
+
def delete_properties(original_schema, path)
|
204
|
+
return original_schema if path == '.'
|
205
|
+
|
206
|
+
unless path_exists?(original_schema, path)
|
207
|
+
puts "Error: Path '#{path}' does not exist in the original schema"
|
208
|
+
return original_schema
|
209
|
+
end
|
210
|
+
|
211
|
+
path_segments = parse_path(path)
|
212
|
+
current_segment = original_schema
|
213
|
+
last_segment = path_segments.pop
|
214
|
+
|
215
|
+
# Navigate to the parent of the last segment in the path
|
216
|
+
path_segments.each do |segment|
|
217
|
+
if current_segment.is_a?(Array)
|
218
|
+
index = segment.to_s.match(/\[(\d+)\]|\d+/)[1]
|
219
|
+
if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length
|
220
|
+
current_segment = current_segment[index.to_i]
|
221
|
+
else
|
222
|
+
puts "Error: Invalid index in path '#{path}'"
|
223
|
+
return original_schema
|
224
|
+
end
|
225
|
+
elsif current_segment.is_a?(Hash) && current_segment.key?(segment)
|
226
|
+
current_segment = current_segment[segment]
|
227
|
+
else
|
228
|
+
puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'"
|
229
|
+
return original_schema
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Delete the last segment in the path
|
234
|
+
if current_segment.is_a?(Array)
|
235
|
+
index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1]
|
236
|
+
if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length
|
237
|
+
current_segment.delete_at(index.to_i)
|
238
|
+
else
|
239
|
+
puts "Error: Invalid index in path '#{path}'"
|
240
|
+
end
|
241
|
+
else
|
242
|
+
current_segment.delete(last_segment)
|
243
|
+
end
|
244
|
+
|
245
|
+
original_schema
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
data/lib/schemable/version.rb
CHANGED