mortymer 0.0.10 → 0.0.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 +4 -4
- data/lib/mortymer/contract.rb +18 -2
- data/lib/mortymer/model.rb +2 -2
- data/lib/mortymer/sigil.rb +92 -0
- data/lib/mortymer/struct_compiler.rb +110 -0
- data/lib/mortymer/version.rb +1 -1
- data/lib/mortymer.rb +4 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aaae1b0260575ca60caeb8b676577f39766162d8127a6266e1c49e168e11265
|
4
|
+
data.tar.gz: 0f92cfaa12e157dcb4954259dd284b7b46f7305dd912d84a55fef597107997f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5c3932bfb5b0daf315af6f8f4a1ef36b0a448229fd15b9c71f7c8699d8f41fcfeb8a5858686b55d6c7905628936908512e97537b6a5ec0024061f6b3f4bcd60
|
7
|
+
data.tar.gz: df393725ff3c29b36ede225c046f8f27cbfea156f09917cd1976b8c3188f9dafa5e57e0aa0876a378f7afda62ab0ea06b17f500d0f46f6d8caae18c249c13acf
|
data/lib/mortymer/contract.rb
CHANGED
@@ -4,11 +4,14 @@ require_relative "moldeable"
|
|
4
4
|
require "dry/validation"
|
5
5
|
require "dry/validation/contract"
|
6
6
|
require_relative "generator"
|
7
|
+
require_relative "types"
|
8
|
+
require_relative "struct_compiler"
|
7
9
|
|
8
10
|
module Mortymer
|
9
11
|
# A base model for defining schemas
|
10
12
|
class Contract < Dry::Validation::Contract
|
11
13
|
include Mortymer::Moldeable
|
14
|
+
include Mortymer::Types
|
12
15
|
|
13
16
|
# Exception raised when an error occours in a contract
|
14
17
|
class ContractError < StandardError
|
@@ -20,15 +23,28 @@ module Mortymer
|
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
26
|
+
def self.__internal_struct_repr__
|
27
|
+
@__internal_struct_repr__ || StructCompiler.new.compile(schema.json_schema)
|
28
|
+
end
|
29
|
+
|
23
30
|
def self.json_schema
|
24
31
|
Generator.new.from_validation(self)
|
25
32
|
end
|
26
33
|
|
27
34
|
def self.structify(params)
|
28
35
|
result = new.call(params)
|
29
|
-
raise ContractError
|
36
|
+
raise ContractError, result.errors.to_h unless result.errors.empty?
|
37
|
+
|
38
|
+
__internal_struct_repr__.new(**result.to_h)
|
39
|
+
end
|
30
40
|
|
31
|
-
|
41
|
+
def self.compile!
|
42
|
+
# Force eager compilation of the internal struct representation.
|
43
|
+
# This provides an optimization by precompiling the struct when
|
44
|
+
# the class is defined rather than waiting for the first use.
|
45
|
+
# The compilation result is memoized, so subsequent accesses
|
46
|
+
# will reuse the compiled struct.
|
47
|
+
__internal_struct_repr__
|
32
48
|
end
|
33
49
|
end
|
34
50
|
end
|
data/lib/mortymer/model.rb
CHANGED
@@ -9,14 +9,14 @@ module Mortymer
|
|
9
9
|
# A base model for defining schemas
|
10
10
|
class Model < Dry::Struct
|
11
11
|
include Mortymer::Moldeable
|
12
|
-
include
|
12
|
+
include Mortymer::Types
|
13
13
|
|
14
14
|
def self.json_schema
|
15
15
|
Generator.new.from_struct(self)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.structify(params)
|
19
|
-
|
19
|
+
call(params)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mortymer
|
4
|
+
# Sigil provides symbolic type checking for input and outputs
|
5
|
+
# of method calls using dry-types
|
6
|
+
module Sigil
|
7
|
+
class TypeError < StandardError; end
|
8
|
+
|
9
|
+
# Class methods to be included as part of the dsl
|
10
|
+
module ClassMethods
|
11
|
+
# Store type signatures for methods before they are defined
|
12
|
+
def sign(*positional_types, returns: nil, **keyword_types)
|
13
|
+
@pending_type_signature = {
|
14
|
+
positional_types: positional_types,
|
15
|
+
keyword_types: keyword_types,
|
16
|
+
returns: returns
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Hook called when a method is defined
|
21
|
+
def method_added(method_name)
|
22
|
+
return super if @pending_type_signature.nil?
|
23
|
+
return super if @processing_type_check
|
24
|
+
|
25
|
+
signature = @pending_type_signature
|
26
|
+
@pending_type_signature = nil
|
27
|
+
|
28
|
+
# Get the original method
|
29
|
+
original_method = instance_method(method_name)
|
30
|
+
|
31
|
+
@processing_type_check = true
|
32
|
+
|
33
|
+
# Redefine the method with type checking
|
34
|
+
define_method(method_name) do |*args, **kwargs|
|
35
|
+
# Validate positional arguments
|
36
|
+
procced_args = []
|
37
|
+
procced_kwargs = {}
|
38
|
+
args.each_with_index do |arg, idx|
|
39
|
+
unless (type = signature[:positional_types][idx])
|
40
|
+
procced_args << arg
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
begin
|
45
|
+
procced_args << (type.respond_to?(:structify) ? type.structify(arg) : type.call(arg))
|
46
|
+
rescue Dry::Types::CoercionError => e
|
47
|
+
raise TypeError, "Invalid type for argument #{idx}: expected #{type}, got #{arg.class} - #{e.message}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validate keyword arguments
|
52
|
+
kwargs.each do |key, value|
|
53
|
+
unless (type = signature[:keyword_types][key])
|
54
|
+
procced_kwargs[key] = value
|
55
|
+
next
|
56
|
+
end
|
57
|
+
|
58
|
+
begin
|
59
|
+
procced_kwargs[key] = (type.respond_to?(:structify) ? type.structify(value) : type.call(value))
|
60
|
+
rescue Dry::Types::CoercionError => e
|
61
|
+
raise TypeError,
|
62
|
+
"Invalid type for keyword argument #{key}: expected #{type}, got #{value.class} - #{e.message}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Call the original method
|
67
|
+
result = original_method.bind(self).call(*procced_args, **procced_kwargs)
|
68
|
+
|
69
|
+
# Validate return type if specified
|
70
|
+
if (return_type = signature[:returns])
|
71
|
+
begin
|
72
|
+
return return_type.respond_to?(:structify) ? return_type.structify(result) : return_type.call(result)
|
73
|
+
rescue Dry::Types::CoercionError => e
|
74
|
+
raise TypeError, "Invalid return type: expected #{return_type}, got #{result.class} - #{e.message}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
result
|
79
|
+
end
|
80
|
+
|
81
|
+
@processing_type_check = false
|
82
|
+
|
83
|
+
# Call super to maintain compatibility with other method hooks
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.included(base)
|
89
|
+
base.extend(ClassMethods)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-struct"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
module Mortymer
|
7
|
+
class StructCompiler
|
8
|
+
PRIMITIVE_TYPE_MAP = {
|
9
|
+
"string" => Mortymer::Model::String,
|
10
|
+
"integer" => Mortymer::Model::Integer,
|
11
|
+
"number" => Mortymer::Model::Float,
|
12
|
+
"boolean" => Mortymer::Model::Bool,
|
13
|
+
"null" => Mortymer::Model::Nil,
|
14
|
+
string: Mortymer::Model::String,
|
15
|
+
integer: Mortymer::Model::Integer,
|
16
|
+
number: Mortymer::Model::Float,
|
17
|
+
boolean: Mortymer::Model::Bool,
|
18
|
+
null: Mortymer::Model::Nil
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def initialize(class_name = "GeneratedStruct#{SecureRandom.hex(4)}")
|
22
|
+
@class_name = class_name
|
23
|
+
@types = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def compile(schema)
|
27
|
+
build_type(schema, @class_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_type(schema, type_name)
|
33
|
+
schema = normalize_schema(schema)
|
34
|
+
case schema["type"]
|
35
|
+
when "object"
|
36
|
+
build_object_type(schema, type_name)
|
37
|
+
when "array"
|
38
|
+
build_array_type(schema)
|
39
|
+
else
|
40
|
+
build_primitive_type(schema)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def normalize_schema(schema)
|
45
|
+
return {} if schema.nil?
|
46
|
+
|
47
|
+
schema = schema.transform_keys(&:to_s)
|
48
|
+
if schema["properties"]
|
49
|
+
schema["properties"] = schema["properties"].transform_keys(&:to_s)
|
50
|
+
schema["properties"].each_value do |prop_schema|
|
51
|
+
normalize_schema(prop_schema)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
schema["items"] = normalize_schema(schema["items"]) if schema["items"]
|
55
|
+
schema["required"] = schema["required"].map(&:to_s) if schema["required"]
|
56
|
+
if schema["enum"]
|
57
|
+
schema["enum"] = schema["enum"].map { |v| v.is_a?(Symbol) ? v.to_s : v }
|
58
|
+
end
|
59
|
+
schema
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_object_type(schema, type_name)
|
63
|
+
return {} unless schema["properties"]
|
64
|
+
|
65
|
+
# Build attribute definitions
|
66
|
+
attributes = schema["properties"].map do |name, property_schema|
|
67
|
+
name = name.to_s # Ensure name is a string
|
68
|
+
nested_type_name = camelize("#{type_name}#{camelize(name)}")
|
69
|
+
type = if property_schema["type"] == "object"
|
70
|
+
build_type(property_schema, nested_type_name)
|
71
|
+
else
|
72
|
+
build_type(property_schema, nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
required = schema["required"]&.include?(name)
|
76
|
+
[name, required ? type : type.optional, required]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a new Struct class for this object
|
80
|
+
Class.new(Mortymer::Model) do
|
81
|
+
attributes.each do |name, type, required|
|
82
|
+
if required
|
83
|
+
attribute name.to_sym, type
|
84
|
+
else
|
85
|
+
attribute? name.to_sym, type
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_array_type(schema)
|
92
|
+
item_type = build_type(schema["items"], nil)
|
93
|
+
Mortymer::Model::Array.of(item_type)
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_primitive_type(schema)
|
97
|
+
type_class = PRIMITIVE_TYPE_MAP[schema["type"]] || Mortymer::Model::Any
|
98
|
+
|
99
|
+
if schema["enum"]
|
100
|
+
type_class.enum(*schema["enum"])
|
101
|
+
else
|
102
|
+
type_class
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def camelize(string)
|
107
|
+
string.split(/[^a-zA-Z0-9]/).map(&:capitalize).join
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/mortymer/version.rb
CHANGED
data/lib/mortymer.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
# typed: true
|
3
3
|
|
4
4
|
require "dry/struct"
|
5
|
+
require "dry/schema"
|
6
|
+
Dry::Schema.load_extensions(:json_schema)
|
5
7
|
require "mortymer/types"
|
6
8
|
require "mortymer/uploaded_file"
|
7
9
|
require "mortymer/uploaded_files"
|
@@ -18,5 +20,7 @@ require "mortymer/openapi_generator"
|
|
18
20
|
require "mortymer/container"
|
19
21
|
require "mortymer/dependencies_dsl"
|
20
22
|
require "mortymer/security_schemes"
|
23
|
+
require "mortymer/struct_compiler"
|
24
|
+
require "mortymer/sigil"
|
21
25
|
require "mortymer/rails" if defined?(Rails)
|
22
26
|
require "mortymer/railtie" if defined?(Rails::Railtie)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mortymer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Gonzalez
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-31 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-struct
|
@@ -115,6 +115,8 @@ files:
|
|
115
115
|
- lib/mortymer/rails/routes.rb
|
116
116
|
- lib/mortymer/railtie.rb
|
117
117
|
- lib/mortymer/security_schemes.rb
|
118
|
+
- lib/mortymer/sigil.rb
|
119
|
+
- lib/mortymer/struct_compiler.rb
|
118
120
|
- lib/mortymer/types.rb
|
119
121
|
- lib/mortymer/uploaded_file.rb
|
120
122
|
- lib/mortymer/uploaded_files.rb
|