dimescheduler 0.1.0

Official Rust client library for the Dime.Scheduler API.
Documentation
"""Unit tests for codegen.py. Run from packages/rust: python scripts/test_codegen.py"""
import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import codegen as g  # noqa: E402


class Naming(unittest.TestCase):
    def test_snake_case(self):
        cases = {
            "jobNo": "job_no", "backofficeID": "backoffice_id", "isMSEntraID": "is_ms_entra_id",
            "planningUOM": "planning_uom", "mboc_Id": "mboc_id", "Error": "error",
            "freeText1": "free_text1", "siteAddressGeoLong": "site_address_geo_long", "url1": "url1",
        }
        for src, want in cases.items():
            self.assertEqual(g.snake(src), want, src)

    def test_keyword_fields_get_trailing_underscore(self):
        self.assertEqual(g.field_name("type"), "type_")
        self.assertEqual(g.field_name("name"), "name")

    def test_rename_needed_only_when_camel_roundtrip_fails(self):
        self.assertIsNone(g.serde_rename("jobNo"))
        self.assertIsNone(g.serde_rename("freeText1"))
        self.assertIsNone(g.serde_rename("type"))  # serde camelCase drops the trailing _ of type_ itself
        self.assertEqual(g.serde_rename("backofficeID"), "backofficeID")
        self.assertEqual(g.serde_rename("Error"), "Error")

    def test_module_assignment(self):
        self.assertEqual(g.module_of("Entities.Task"), ("entities", "Task"))
        self.assertEqual(g.module_of("Models.Task"), ("models", "Task"))
        self.assertEqual(g.module_of("Core.TaskDto"), ("types", "TaskDto"))
        self.assertEqual(
            g.module_of("Dime.Scheduler.Api.Models.TaskAssignmentRequest"), ("types", "TaskAssignmentRequest")
        )
        self.assertEqual(g.module_of("DimeSchedulerApiResponse"), ("types", "DimeSchedulerApiResponse"))


class Types(unittest.TestCase):
    def test_scalar_mapping(self):
        self.assertEqual(g.rust_type({"type": "string", "format": "date-time"}, {}), "String")
        self.assertEqual(g.rust_type({"type": "integer"}, {}), "i32")
        self.assertEqual(g.rust_type({"type": "integer", "format": "int64"}, {}), "i64")
        self.assertEqual(g.rust_type({"type": "number", "format": "double"}, {}), "f64")
        self.assertEqual(g.rust_type({"type": "boolean"}, {}), "bool")
        self.assertEqual(g.rust_type({"type": "array", "items": {"type": "string"}}, {}), "Vec<String>")
        self.assertEqual(
            g.rust_type({"type": "object", "additionalProperties": {"type": "number", "format": "double"}}, {}),
            "HashMap<String, f64>",
        )

    def test_typeless_schema_is_json_value(self):
        self.assertEqual(g.rust_type({"nullable": True}, {}), "serde_json::Value")
        self.assertEqual(g.rust_type({}, {}), "serde_json::Value")

    def test_ref_is_crate_qualified(self):
        self.assertEqual(g.rust_type({"$ref": "#/components/schemas/Models.Task"}, {}), "crate::models::Task")

    def test_unknown_construct_fails_with_schema_name(self):
        with self.assertRaises(SystemExit) as cm:
            g.rust_type({"oneOf": [{"type": "string"}]}, {}, where="Entities.Thing.prop")
        self.assertIn("Entities.Thing.prop", str(cm.exception))


class Enums(unittest.TestCase):
    def test_named_enum(self):
        out = g.render_enum("SchedulingMode", "Entities.SchedulingMode",
                            {"type": "integer", "enum": [0, 1], "x-enumNames": ["Normal", "Manual"]})
        self.assertIn("#[repr(i32)]", out)
        self.assertIn("#[default]\n    Normal = 0,", out)
        self.assertIn("Manual = 1,", out)

    def test_default_is_lowest_value_not_first_listed(self):
        out = g.render_enum("X", "Models.X", {"type": "integer", "enum": [5, 2], "x-enumNames": ["Five", "Two"]})
        self.assertIn("#[default]\n    Two = 2,", out)

    def test_aliased_enum_becomes_newtype(self):
        out = g.render_enum("HttpStatusCode", "System.Net.HttpStatusCode",
                            {"type": "integer", "enum": [300, 301],
                             "x-enumNames": ["MultipleChoices", "Ambiguous", "MovedPermanently"]})
        self.assertIn("pub struct HttpStatusCode(pub i32);", out)
        self.assertIn("#[serde(transparent)]", out)
        self.assertIn("aliases", out)  # the doc comment explains why


class Cycles(unittest.TestCase):
    def test_boxes_only_cycle_edges(self):
        schemas = {
            "Models.A": {"type": "object", "properties": {"b": {"$ref": "#/components/schemas/Models.B"}}},
            "Models.B": {"type": "object", "properties": {"a": {"$ref": "#/components/schemas/Models.A"},
                                                          "c": {"$ref": "#/components/schemas/Models.C"}}},
            "Models.C": {"type": "object", "properties": {"n": {"type": "integer"}}},
        }
        boxed = g.cycle_edges(schemas)
        self.assertIn(("Models.A", "Models.B"), boxed)
        self.assertIn(("Models.B", "Models.A"), boxed)
        self.assertNotIn(("Models.B", "Models.C"), boxed)

    def test_self_reference_is_boxed(self):
        schemas = {"Models.T": {"type": "object", "properties": {"parent": {"$ref": "#/components/schemas/Models.T"}}}}
        self.assertIn(("Models.T", "Models.T"), g.cycle_edges(schemas))


class Collisions(unittest.TestCase):
    def test_duplicate_name_in_module_fails(self):
        with self.assertRaises(SystemExit) as cm:
            g.plan_modules({"Core.X": {"type": "object"}, "Dime.Scheduler.X": {"type": "object"}})
        self.assertIn("types", str(cm.exception))
        self.assertIn("X", str(cm.exception))


if __name__ == "__main__":
    unittest.main()