1+ from typing import Type , Dict , Any , get_args , Union , Optional , Tuple
2+ from osbot_utils .type_safe .Type_Safe import Type_Safe
3+ from osbot_utils .type_safe .Type_Safe__Primitive import Type_Safe__Primitive
4+ from osbot_utils .type_safe .primitives .safe_str .Safe_Str import Safe_Str
5+ from osbot_utils .type_safe .primitives .safe_int .Safe_Int import Safe_Int
6+ from osbot_utils .type_safe .primitives .safe_float .Safe_Float import Safe_Float
7+ from osbot_utils .type_safe .type_safe_core .decorators .type_safe import type_safe
8+ from osbot_utils .type_safe .type_safe_core .shared .Type_Safe__Cache import type_safe_cache
9+
10+
11+ class Type_Safe__To__Json (Type_Safe ): # Converts Type_Safe classes to JSON Schema (draft-07 compatible)
12+ schema_cache : Dict [Tuple [Type , bool ], Dict [str , Any ]]
13+ include_defaults : bool = True
14+ include_examples : bool = False
15+ strict_mode : bool = False # If True, includes all Type_Safe constraints
16+
17+ @type_safe
18+ def convert_class (self , type_safe_class : Type [Type_Safe ] , # Type_Safe class to convert
19+ title : str = None , # Optional schema title
20+ description : str = None , # Optional schema description
21+ is_nested : bool = False
22+ ) -> Dict [str , Any ]: # Returns JSON Schema
23+
24+
25+ cache_key = (type_safe_class , is_nested ) # Create a cache key that includes whether it's nested
26+
27+ if cache_key in self .schema_cache :
28+ return self .schema_cache [cache_key ]
29+
30+ if type_safe_class in self .schema_cache : # Check cache first
31+ return self .schema_cache [type_safe_class ]
32+
33+ schema = { "type" : "object" ,
34+ "title" : title or type_safe_class .__name__ ,
35+ "additionalProperties" : False } # Type_Safe classes are strict
36+
37+ if not is_nested :
38+ schema ["$schema" ] = "http://json-schema.org/draft-07/schema#" # Only add $schema for root level (non-nested) objects
39+
40+ if description :
41+ schema ["description" ] = description
42+
43+ properties = {}
44+ required = []
45+
46+ annotations = type_safe_cache .get_class_annotations (type_safe_class )
47+ cls_kwargs = type_safe_class .__cls_kwargs__ ()
48+
49+ for field_name , field_type in annotations :
50+ property_schema = self .convert_field_type (field_type )
51+ properties [field_name ] = property_schema
52+
53+ if hasattr (type_safe_class , '__annotations_comments__' ): # Add description from docstring if available
54+ comments = type_safe_class .__annotations_comments__
55+ if field_name in comments :
56+ property_schema ["description" ] = comments [field_name ]
57+
58+ if field_name not in cls_kwargs : # Check if field is required (no default value)
59+ required .append (field_name )
60+ else :
61+ default_value = cls_kwargs [field_name ]
62+ if self .include_defaults and default_value is not None :
63+ if isinstance (default_value , (str , int , float , bool )): # Add default to schema if it's a simple type
64+ property_schema ["default" ] = default_value
65+
66+ schema ["properties" ] = properties
67+ if required :
68+ schema ["required" ] = required
69+
70+ self .schema_cache [cache_key ] = schema
71+ return schema
72+
73+ @type_safe
74+ def convert_field_type (self , field_type : Any # Field type to convert
75+ ) -> Dict [str , Any ]: # Returns JSON Schema for field
76+
77+ origin = type_safe_cache .get_origin (field_type )
78+
79+ if field_type is str : # Handle primitive types
80+ return {"type" : "string" }
81+ elif field_type is int :
82+ return {"type" : "integer" }
83+ elif field_type is float :
84+ return {"type" : "number" }
85+ elif field_type is bool :
86+ return {"type" : "boolean" }
87+
88+ if isinstance (field_type , type ) and issubclass (field_type , Type_Safe__Primitive ): # Handle Type_Safe__Primitive subclasses with their constraints
89+ schema = self .extract_primitive_schema (field_type )
90+ return schema
91+
92+ if origin is list : # Handle list/array types
93+ args = get_args (field_type )
94+ if args :
95+ return { "type" : "array" ,
96+ "items" : self .convert_field_type (args [0 ]) }
97+ return {"type" : "array" }
98+
99+ if origin is dict :
100+ args = get_args (field_type ) # Handle dict/object types
101+ if len (args ) == 2 :
102+ return { "type" : "object" , # JSON Schema doesn't support typed dict keys, so we use additionalProperties
103+ "additionalProperties" : self .convert_field_type (args [1 ]) }
104+ return {"type" : "object" }
105+
106+ if origin is set : # Handle set types (convert to array with uniqueItems)
107+ args = get_args (field_type )
108+ schema = { "type" : "array" ,
109+ "uniqueItems" : True }
110+ if args :
111+ schema ["items" ] = self .convert_field_type (args [0 ])
112+ return schema
113+
114+ if origin in (Union , Optional ): # Handle Union/Optional types
115+ args = get_args (field_type )
116+
117+ if type (None ) in args : # Special case for Optional (Union with None)
118+ non_none_args = [arg for arg in args if arg is not type (None )]
119+ if len (non_none_args ) == 1 :
120+ schema = self .convert_field_type (non_none_args [0 ]) # Optional single type
121+ schema ["nullable" ] = True # JSON Schema draft-07 style
122+ return schema
123+
124+ return { "oneOf" : [self .convert_field_type (arg ) for arg in args ] } # General Union case
125+
126+ if isinstance (field_type , type ) and issubclass (field_type , Type_Safe ): # This will register it in components_cache and return a $ref
127+ return self .convert_class (field_type , is_nested = True ) # Call convert_class with is_nested=True for nested objects
128+
129+ return {"type" : "object" } # Default fallback
130+
131+ @type_safe
132+ def extract_primitive_schema (self , primitive_class : Type [Type_Safe__Primitive ] # Primitive class to analyze
133+ ) -> Dict [str , Any ]: # Returns schema with constraints
134+
135+ base_type = primitive_class .__primitive_base__
136+
137+ if base_type is str : # Start with base type schema
138+ schema = {"type" : "string" }
139+ elif base_type is int :
140+ schema = {"type" : "integer" }
141+ elif base_type is float :
142+ schema = {"type" : "number" }
143+ else :
144+ schema = {"type" : "string" } # Default fallback
145+
146+ if issubclass (primitive_class , Safe_Str ): # Extract constraints from Safe_Str types
147+ if hasattr (primitive_class , 'max_length' ):
148+ schema ['maxLength' ] = primitive_class .max_length
149+ if hasattr (primitive_class , 'regex' ) and self .strict_mode :
150+ if hasattr (primitive_class .regex , 'pattern' ): # Only include regex pattern in strict mode
151+ schema ['pattern' ] = primitive_class .regex .pattern
152+
153+ if issubclass (primitive_class , Safe_Int ): # Extract constraints from Safe_Int types
154+ if hasattr (primitive_class , 'min_value' ) and primitive_class .min_value is not None :
155+ schema ['minimum' ] = primitive_class .min_value
156+ if hasattr (primitive_class , 'max_value' ) and primitive_class .max_value is not None :
157+ schema ['maximum' ] = primitive_class .max_value
158+
159+ if issubclass (primitive_class , Safe_Float ): # Extract constraints from Safe_Float types
160+ if hasattr (primitive_class , 'min_value' ) and primitive_class .min_value is not None :
161+ schema ['minimum' ] = primitive_class .min_value
162+ if hasattr (primitive_class , 'max_value' ) and primitive_class .max_value is not None :
163+ schema ['maximum' ] = primitive_class .max_value
164+
165+ return schema
166+
167+ @type_safe
168+ def convert_to_json_schema_string (self , type_safe_class : Type [Type_Safe ] # Class to convert
169+ ) -> str : # Returns JSON Schema as string
170+ import json
171+ schema = self .convert_class (type_safe_class )
172+ return json .dumps (schema , indent = 2 )
173+
174+ @type_safe
175+ def validate_against_schema (self , instance : Type_Safe , # Instance to validate
176+ schema : Dict [str , Any ] # Schema to validate against
177+ ) -> bool : # Returns True if valid | Validate a Type_Safe instance against a JSON schema
178+ try :
179+ import jsonschema
180+ instance_data = instance .json ()
181+ jsonschema .validate (instance_data , schema )
182+ return True
183+ except ImportError :
184+ raise ImportError ("jsonschema package required for validation" )
185+ except jsonschema .ValidationError :
186+ return False
187+
188+ type_safe__to__json = Type_Safe__To__Json () # Singleton instance
0 commit comments