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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c49e9b245a1153aff2fe7a3e8dfe0a4c9bfd60946508bb5370ecc2a9bd40fdd
4
- data.tar.gz: a3ef9f90da8ed2201b0a14d93fef1a71b4c4219bf46a9df92354e2537291fc48
3
+ metadata.gz: 1aaae1b0260575ca60caeb8b676577f39766162d8127a6266e1c49e168e11265
4
+ data.tar.gz: 0f92cfaa12e157dcb4954259dd284b7b46f7305dd912d84a55fef597107997f7
5
5
  SHA512:
6
- metadata.gz: 589b5dfd53e5aff161283d4bebff4aaa3d33d6c801500898e5042017a6520fa78933ddf473cd6396ee34938fcc4e264b6c825c0b42a3066cc5fa672793cafcc2
7
- data.tar.gz: 9770e1bcd6a3d334c62864a144802cf6523e8713aafd57f7d25d81b9f6341221e22107cbacdfce8c6330f6b83b6ad7b4155878fd21b00bead97236e8d4d2aae5
6
+ metadata.gz: c5c3932bfb5b0daf315af6f8f4a1ef36b0a448229fd15b9c71f7c8699d8f41fcfeb8a5858686b55d6c7905628936908512e97537b6a5ec0024061f6b3f4bcd60
7
+ data.tar.gz: df393725ff3c29b36ede225c046f8f27cbfea156f09917cd1976b8c3188f9dafa5e57e0aa0876a378f7afda62ab0ea06b17f500d0f46f6d8caae18c249c13acf
@@ -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.new(result.errors.to_h) unless result.errors.empty?
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
- result.to_h
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
@@ -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 Dry.Types()
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
- new(params)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mortymer
4
- VERSION = "0.0.10"
4
+ VERSION = "0.0.11"
5
5
  end
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.10
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-24 00:00:00.000000000 Z
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