django-cfg 1.4.72__py3-none-any.whl → 1.4.73__py3-none-any.whl

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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

@@ -0,0 +1,260 @@
1
+ """
2
+ Proto Messages Generator - Generates Protocol Buffer message definitions from IR schemas.
3
+
4
+ Converts IRSchemaObject instances into proto3 message definitions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from .type_mapper import ProtoTypeMapper
12
+
13
+ if TYPE_CHECKING:
14
+ from django_cfg.modules.django_client.core.ir.schema import IRSchemaObject
15
+
16
+
17
+ class ProtoMessagesGenerator:
18
+ """
19
+ Generates Protocol Buffer message definitions from IR schemas.
20
+
21
+ Handles:
22
+ - Basic message structure with fields
23
+ - Nested message definitions
24
+ - Enums (from string enums in OpenAPI)
25
+ - Field numbering
26
+ - Proper indentation and formatting
27
+ """
28
+
29
+ def __init__(self, type_mapper: ProtoTypeMapper):
30
+ self.type_mapper = type_mapper
31
+ self.generated_messages: set[str] = set() # Track what we've generated
32
+ self.message_definitions: list[str] = [] # Ordered list of definitions
33
+
34
+ def generate_message(
35
+ self, schema: IRSchemaObject, message_name: str | None = None
36
+ ) -> str:
37
+ """
38
+ Generate a proto message from an IR schema.
39
+
40
+ Args:
41
+ schema: IR schema object to convert
42
+ message_name: Override message name (uses schema.name if not provided)
43
+
44
+ Returns:
45
+ Proto message definition string
46
+ """
47
+ if message_name is None:
48
+ message_name = self.type_mapper.get_message_name(schema.name or "Message")
49
+
50
+ # Skip if already generated
51
+ if message_name in self.generated_messages:
52
+ return ""
53
+
54
+ self.generated_messages.add(message_name)
55
+
56
+ # Handle different schema types
57
+ if schema.type == "object":
58
+ return self._generate_object_message(schema, message_name)
59
+ elif schema.type == "array":
60
+ # Arrays are handled as repeated fields, not separate messages
61
+ return ""
62
+ elif schema.enum:
63
+ return self._generate_enum(schema, message_name)
64
+ else:
65
+ # Scalar types don't need messages
66
+ return ""
67
+
68
+ def _generate_object_message(self, schema: IRSchemaObject, message_name: str) -> str:
69
+ """Generate a message for an object schema."""
70
+ lines = [f"message {message_name} {{"]
71
+
72
+ # Generate nested enums first
73
+ for prop_name, prop_schema in (schema.properties or {}).items():
74
+ if prop_schema.enum:
75
+ enum_name = self.type_mapper.get_message_name(prop_name)
76
+ nested_enum = self._generate_enum(prop_schema, enum_name, indent=2)
77
+ if nested_enum:
78
+ lines.append("")
79
+ lines.extend(f" {line}" for line in nested_enum.split("\n"))
80
+
81
+ # Generate nested messages (only if not already defined at top level)
82
+ for prop_name, prop_schema in (schema.properties or {}).items():
83
+ if prop_schema.type == "object" and not prop_schema.enum:
84
+ nested_name = self.type_mapper.get_message_name(prop_name)
85
+ # Skip if this message is already generated (it's a top-level schema)
86
+ if nested_name not in self.generated_messages:
87
+ self.generated_messages.add(nested_name)
88
+ nested_msg = self._generate_object_message(prop_schema, nested_name)
89
+ if nested_msg:
90
+ lines.append("")
91
+ lines.extend(f" {line}" for line in nested_msg.split("\n"))
92
+
93
+ # Generate fields
94
+ field_number = 1
95
+ if schema.properties:
96
+ lines.append("")
97
+ for prop_name, prop_schema in schema.properties.items():
98
+ field_def = self._generate_field(
99
+ prop_name, prop_schema, field_number, schema.required or []
100
+ )
101
+ lines.append(f" {field_def}")
102
+ field_number += 1
103
+
104
+ lines.append("}")
105
+
106
+ definition = "\n".join(lines)
107
+ self.message_definitions.append(definition)
108
+ return definition
109
+
110
+ def _generate_field(
111
+ self,
112
+ field_name: str,
113
+ field_schema: IRSchemaObject,
114
+ field_number: int,
115
+ required_fields: list[str],
116
+ ) -> str:
117
+ """
118
+ Generate a single field definition.
119
+
120
+ Args:
121
+ field_name: Original field name
122
+ field_schema: Field schema
123
+ field_number: Proto field number
124
+ required_fields: List of required field names
125
+
126
+ Returns:
127
+ Field definition line (e.g., "optional string name = 1;")
128
+ """
129
+ # Sanitize field name
130
+ proto_field_name = self.type_mapper.sanitize_field_name(field_name)
131
+
132
+ # Determine if field is required/nullable
133
+ is_required = field_name in required_fields
134
+ is_nullable = field_schema.nullable or False
135
+ is_repeated = field_schema.type == "array"
136
+
137
+ # Get field type
138
+ if is_repeated:
139
+ # Array field - use items type
140
+ if field_schema.items:
141
+ if field_schema.items.type == "object":
142
+ # Nested object array
143
+ item_type = self.type_mapper.get_message_name(field_name + "Item")
144
+ # Generate the nested message
145
+ self.generate_message(field_schema.items, item_type)
146
+ elif field_schema.items.enum:
147
+ # Enum array - generate the enum definition
148
+ item_type = self.type_mapper.get_message_name(field_name)
149
+ # Generate the enum if not already generated
150
+ if item_type not in self.generated_messages:
151
+ self.generated_messages.add(item_type)
152
+ enum_def = self._generate_enum(field_schema.items, item_type)
153
+ if enum_def:
154
+ self.message_definitions.append(enum_def)
155
+ else:
156
+ # Scalar array
157
+ item_type = self.type_mapper.map_type(
158
+ field_schema.items.type or "string",
159
+ field_schema.items.format,
160
+ )
161
+ else:
162
+ item_type = "string" # Fallback
163
+ field_type = item_type
164
+ elif field_schema.type == "object":
165
+ # Nested object
166
+ field_type = self.type_mapper.get_message_name(field_name)
167
+ elif field_schema.enum:
168
+ # Enum field
169
+ field_type = self.type_mapper.get_message_name(field_name)
170
+ else:
171
+ # Scalar field
172
+ field_type = self.type_mapper.map_type(
173
+ field_schema.type or "string",
174
+ field_schema.format,
175
+ )
176
+
177
+ # Get field label
178
+ label = self.type_mapper.get_field_label(is_required, is_nullable, is_repeated)
179
+
180
+ # Build field definition
181
+ if label:
182
+ return f"{label} {field_type} {proto_field_name} = {field_number};"
183
+ else:
184
+ return f"{field_type} {proto_field_name} = {field_number};"
185
+
186
+ def _generate_enum(
187
+ self, schema: IRSchemaObject, enum_name: str, indent: int = 0
188
+ ) -> str:
189
+ """
190
+ Generate an enum definition.
191
+
192
+ Args:
193
+ schema: Schema with enum values
194
+ enum_name: Enum name
195
+ indent: Indentation level for nested enums
196
+
197
+ Returns:
198
+ Enum definition string
199
+ """
200
+ if not schema.enum:
201
+ return ""
202
+
203
+ indent_str = " " * indent
204
+ lines = [f"{indent_str}enum {enum_name} {{"]
205
+
206
+ # Proto enums must start with 0
207
+ # Add UNKNOWN/UNSPECIFIED as first value if not present
208
+ enum_values = list(schema.enum)
209
+ if not any(
210
+ v.upper() in ("UNKNOWN", "UNSPECIFIED", f"{enum_name}_UNKNOWN")
211
+ for v in enum_values
212
+ ):
213
+ lines.append(f"{indent_str} {enum_name.upper()}_UNKNOWN = 0;")
214
+ start_index = 1
215
+ else:
216
+ start_index = 0
217
+
218
+ # Generate enum values
219
+ for idx, value in enumerate(enum_values, start=start_index):
220
+ # Convert to UPPER_SNAKE_CASE
221
+ enum_value_name = (
222
+ str(value).replace("-", "_").replace(" ", "_").replace(".", "_").upper()
223
+ )
224
+ # Add enum name prefix if not already present
225
+ if not enum_value_name.startswith(enum_name.upper()):
226
+ enum_value_name = f"{enum_name.upper()}_{enum_value_name}"
227
+
228
+ lines.append(f"{indent_str} {enum_value_name} = {idx};")
229
+
230
+ lines.append(f"{indent_str}}}")
231
+
232
+ return "\n".join(lines)
233
+
234
+ def generate_all_messages(self, schemas: dict[str, IRSchemaObject]) -> list[str]:
235
+ """
236
+ Generate all message definitions from a collection of schemas.
237
+
238
+ Args:
239
+ schemas: Dictionary of schema_name -> IRSchemaObject
240
+
241
+ Returns:
242
+ List of proto message definition strings
243
+ """
244
+ self.generated_messages.clear()
245
+ self.message_definitions.clear()
246
+
247
+ for schema_name, schema in schemas.items():
248
+ message_name = self.type_mapper.get_message_name(schema_name)
249
+ self.generate_message(schema, message_name)
250
+
251
+ return self.message_definitions
252
+
253
+ def get_all_definitions(self) -> str:
254
+ """
255
+ Get all generated message definitions as a single string.
256
+
257
+ Returns:
258
+ Combined proto definitions separated by blank lines
259
+ """
260
+ return "\n\n".join(self.message_definitions)
@@ -0,0 +1,295 @@
1
+ """
2
+ Proto Services Generator - Generates gRPC service definitions from IR operations.
3
+
4
+ Converts IROperationObject instances into proto3 service and rpc definitions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from .type_mapper import ProtoTypeMapper
12
+
13
+ if TYPE_CHECKING:
14
+ from django_cfg.modules.django_client.core.ir.operation import IROperationObject
15
+ from django_cfg.modules.django_client.core.ir.schema import IRSchemaObject
16
+
17
+
18
+ class ProtoServicesGenerator:
19
+ """
20
+ Generates gRPC service definitions from IR operations.
21
+
22
+ Handles:
23
+ - Service grouping by tags
24
+ - RPC method definitions
25
+ - Request/Response message generation
26
+ - Empty responses (google.protobuf.Empty)
27
+ - Stream annotations (if needed in future)
28
+ """
29
+
30
+ def __init__(self, type_mapper: ProtoTypeMapper, context: IRContext | None = None):
31
+ self.type_mapper = type_mapper
32
+ self.context = context # Need context to resolve schema references
33
+ self.services: dict[str, list[str]] = {} # service_name -> [rpc_definitions]
34
+ self.request_messages: dict[str, str] = {} # Track generated request messages
35
+
36
+ def generate_rpc(self, operation: IROperationObject) -> tuple[str, list[str]]:
37
+ """
38
+ Generate an RPC definition from an IR operation.
39
+
40
+ Args:
41
+ operation: IR operation object
42
+
43
+ Returns:
44
+ Tuple of (service_name, [request_message_defs, response_message_defs, rpc_def])
45
+ """
46
+ # Determine service name from tags (first tag or "Default")
47
+ service_name = self._get_service_name(operation)
48
+
49
+ # Generate RPC method name
50
+ rpc_name = self._get_rpc_name(operation)
51
+
52
+ # Generate request message
53
+ request_message_name, request_msg_def = self._generate_request_message(
54
+ operation, rpc_name
55
+ )
56
+
57
+ # Generate response message
58
+ response_message_name, response_msg_def = self._generate_response_message(
59
+ operation, rpc_name
60
+ )
61
+
62
+ # Generate RPC definition
63
+ rpc_def = f" rpc {rpc_name}({request_message_name}) returns ({response_message_name});"
64
+
65
+ # Add comment if operation has description
66
+ if operation.description:
67
+ # Sanitize description for proto comment
68
+ desc = operation.description.strip().replace("\n", "\n // ")
69
+ rpc_def = f" // {desc}\n{rpc_def}"
70
+
71
+ # Collect all definitions
72
+ definitions = []
73
+ if request_msg_def:
74
+ definitions.append(request_msg_def)
75
+ if response_msg_def:
76
+ definitions.append(response_msg_def)
77
+ definitions.append(rpc_def)
78
+
79
+ return service_name, definitions
80
+
81
+ def _get_service_name(self, operation: IROperationObject) -> str:
82
+ """Get service name from operation tags."""
83
+ if operation.tags and len(operation.tags) > 0:
84
+ service_name = operation.tags[0]
85
+ else:
86
+ service_name = "Default"
87
+
88
+ # Convert to PascalCase and add "Service" suffix
89
+ return self.type_mapper.get_message_name(service_name) + "Service"
90
+
91
+ def _get_rpc_name(self, operation: IROperationObject) -> str:
92
+ """Get RPC method name from operation."""
93
+ if operation.operation_id:
94
+ # Use operation_id (already should be camelCase from OpenAPI)
95
+ name = operation.operation_id
96
+ else:
97
+ # Fallback: method + path
98
+ path_parts = [p for p in operation.path.split("/") if p and not p.startswith("{")]
99
+ name = operation.method + "_" + "_".join(path_parts)
100
+
101
+ # Ensure PascalCase for RPC method name
102
+ return self.type_mapper.get_message_name(name)
103
+
104
+ def _generate_request_message(
105
+ self, operation: IROperationObject, rpc_name: str
106
+ ) -> tuple[str, str]:
107
+ """
108
+ Generate request message for an RPC.
109
+
110
+ Args:
111
+ operation: IR operation
112
+ rpc_name: RPC method name
113
+
114
+ Returns:
115
+ Tuple of (message_name, message_definition)
116
+ """
117
+ message_name = f"{rpc_name}Request"
118
+
119
+ # Check if we need a request message at all
120
+ has_patch_body = hasattr(operation, 'patch_request_body') and operation.patch_request_body
121
+ has_params = bool(
122
+ operation.parameters
123
+ or operation.request_body
124
+ or has_patch_body
125
+ )
126
+
127
+ if not has_params:
128
+ # No parameters - use empty message or google.protobuf.Empty
129
+ self.type_mapper.imported_types.add("google.protobuf.Empty")
130
+ return "google.protobuf.Empty", ""
131
+
132
+ # Build request message
133
+ lines = [f"message {message_name} {{"]
134
+ field_number = 1
135
+
136
+ # Add path/query parameters
137
+ if operation.parameters:
138
+ for param in operation.parameters:
139
+ if param.location in ("path", "query"):
140
+ field_name = self.type_mapper.sanitize_field_name(param.name)
141
+ field_type = self.type_mapper.map_type(
142
+ param.schema_type,
143
+ None, # IRParameterObject doesn't have format
144
+ )
145
+ is_required = param.required
146
+ is_nullable = False # Parameters don't have nullable in IR
147
+
148
+ label = self.type_mapper.get_field_label(
149
+ is_required, is_nullable, False
150
+ )
151
+
152
+ if label:
153
+ lines.append(
154
+ f" {label} {field_type} {field_name} = {field_number};"
155
+ )
156
+ else:
157
+ lines.append(f" {field_type} {field_name} = {field_number};")
158
+
159
+ field_number += 1
160
+
161
+ # Add request body as a single field referencing the schema
162
+ if operation.request_body:
163
+ # Check if this is multipart/form-data (file upload)
164
+ is_multipart = operation.request_body.content_type and "multipart" in operation.request_body.content_type
165
+
166
+ if is_multipart:
167
+ # For file uploads, use bytes type directly
168
+ # Note: In real gRPC, file uploads should use streaming (not implemented here)
169
+ field_name = "file_data"
170
+ field_type = "bytes"
171
+ is_required = operation.request_body.required
172
+ label = self.type_mapper.get_field_label(is_required, False, False)
173
+
174
+ if label:
175
+ lines.append(f" {label} {field_type} {field_name} = {field_number};")
176
+ else:
177
+ lines.append(f" {field_type} {field_name} = {field_number};")
178
+ else:
179
+ # Use schema_name to reference the request body schema
180
+ schema_name = operation.request_body.schema_name
181
+ field_name = "body"
182
+ field_type = self.type_mapper.get_message_name(schema_name)
183
+
184
+ is_required = operation.request_body.required
185
+ label = self.type_mapper.get_field_label(is_required, False, False)
186
+
187
+ if label:
188
+ lines.append(f" {label} {field_type} {field_name} = {field_number};")
189
+ else:
190
+ lines.append(f" {field_type} {field_name} = {field_number};")
191
+ elif has_patch_body:
192
+ # PATCH operations have optional body
193
+ schema_name = operation.patch_request_body.schema_name
194
+ field_name = "body"
195
+ field_type = self.type_mapper.get_message_name(schema_name)
196
+
197
+ # PATCH body is always optional
198
+ label = "optional"
199
+ lines.append(f" {label} {field_type} {field_name} = {field_number};")
200
+
201
+ lines.append("}")
202
+
203
+ return message_name, "\n".join(lines)
204
+
205
+ def _generate_response_message(
206
+ self, operation: IROperationObject, rpc_name: str
207
+ ) -> tuple[str, str]:
208
+ """
209
+ Generate response message for an RPC.
210
+
211
+ Args:
212
+ operation: IR operation
213
+ rpc_name: RPC method name
214
+
215
+ Returns:
216
+ Tuple of (message_name, message_definition)
217
+ """
218
+ message_name = f"{rpc_name}Response"
219
+
220
+ # Get successful response (200, 201, etc.)
221
+ response_schema_name = None
222
+ for status_code, response in operation.responses.items():
223
+ if isinstance(status_code, int) and 200 <= status_code < 300:
224
+ # Found successful response
225
+ if response.schema_name:
226
+ response_schema_name = response.schema_name
227
+ break
228
+
229
+ # No response body - use Empty
230
+ if not response_schema_name:
231
+ self.type_mapper.imported_types.add("google.protobuf.Empty")
232
+ return "google.protobuf.Empty", ""
233
+
234
+ # Build response message - simply reference the schema
235
+ lines = [f"message {message_name} {{"]
236
+
237
+ # Reference the response schema as a single field
238
+ field_type = self.type_mapper.get_message_name(response_schema_name)
239
+ lines.append(f" {field_type} data = 1;")
240
+
241
+ lines.append("}")
242
+
243
+ return message_name, "\n".join(lines)
244
+
245
+ def generate_all_services(
246
+ self, operations: list[IROperationObject]
247
+ ) -> dict[str, str]:
248
+ """
249
+ Generate all service definitions from operations.
250
+
251
+ Args:
252
+ operations: List of IR operations
253
+
254
+ Returns:
255
+ Dictionary of service_name -> service_definition
256
+ """
257
+ self.services.clear()
258
+ self.request_messages.clear()
259
+
260
+ # Group operations by service
261
+ messages_by_service: dict[str, list[str]] = {}
262
+ rpcs_by_service: dict[str, list[str]] = {}
263
+
264
+ for operation in operations:
265
+ service_name, definitions = self.generate_rpc(operation)
266
+
267
+ if service_name not in messages_by_service:
268
+ messages_by_service[service_name] = []
269
+ rpcs_by_service[service_name] = []
270
+
271
+ # Separate messages from RPC definitions
272
+ for definition in definitions:
273
+ if definition.startswith("message "):
274
+ messages_by_service[service_name].append(definition)
275
+ elif definition.strip().startswith("rpc ") or definition.strip().startswith("//"):
276
+ rpcs_by_service[service_name].append(definition)
277
+
278
+ # Build service definitions
279
+ service_definitions = {}
280
+ for service_name in rpcs_by_service:
281
+ lines = []
282
+
283
+ # Add messages first
284
+ if service_name in messages_by_service:
285
+ lines.extend(messages_by_service[service_name])
286
+ lines.append("") # Blank line between messages and service
287
+
288
+ # Add service definition
289
+ lines.append(f"service {service_name} {{")
290
+ lines.extend(rpcs_by_service[service_name])
291
+ lines.append("}")
292
+
293
+ service_definitions[service_name] = "\n".join(lines)
294
+
295
+ return service_definitions