diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4f9167d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "enableAllProjectMcpServers": false, + "permissions": { + "allow": [ + "Bash(find:*)" + ] + } +} \ No newline at end of file diff --git a/CommandForgeGenerator.Tests/GenerateTestCode.cs b/CommandForgeGenerator.Tests/GenerateTestCode.cs new file mode 100644 index 0000000..d0e1db9 --- /dev/null +++ b/CommandForgeGenerator.Tests/GenerateTestCode.cs @@ -0,0 +1,40 @@ +namespace CommandForgeGenerator.Tests; + +public class GenerateTestCode +{ + public const string TextCommand = """ + #if ENABLE_COMMAND_FORGE_GENERATOR + namespace CommandForgeGenerator.Command + { + public partial class TextCommand : ICommandForgeCommand + { + public const string Type = "text"; + public readonly CommandId CommandId; + + public readonly string Character; + public readonly string Body; + + + public static TextCommand Create(int commandId, global::Newtonsoft.Json.Linq.JToken json) + { + + var Character = (string)json["character"]; + var Body = (string)json["body"]; + + + return new TextCommand(commandId, Character, Body); + } + + public TextCommand(int commandId, string Character, string Body) + { + CommandId = (CommandId)commandId; + + this.Character = Character; + this.Body = Body; + + } + } + } + #endif + """; +} \ No newline at end of file diff --git a/CommandForgeGenerator.Tests/Test.cs b/CommandForgeGenerator.Tests/Test.cs index 976db08..2d2ba98 100644 --- a/CommandForgeGenerator.Tests/Test.cs +++ b/CommandForgeGenerator.Tests/Test.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using CommandForgeGenerator.Generator.CodeGenerate; using CommandForgeGenerator.Generator.Json; using CommandForgeGenerator.Generator.Semantic; @@ -64,6 +65,7 @@ public void GenerateTest() var codeFiles = CodeGenerator.Generate(commandsSchema); Assert.Equal(16, codeFiles.Count); + Assert.Equal(GenerateTestCode.TextCommand, codeFiles.FirstOrDefault(c => c.FileName == "TextCommand.g.cs").Code); #region Internal @@ -84,7 +86,6 @@ string GetSampleYaml() body: type: string multiline: true - required: true - id: emote label: エモート diff --git a/CommandForgeGenerator/CodeGenerate/CodeGenerator.cs b/CommandForgeGenerator/CodeGenerate/CodeGenerator.cs index 72a07ce..745938c 100644 --- a/CommandForgeGenerator/CodeGenerate/CodeGenerator.cs +++ b/CommandForgeGenerator/CodeGenerate/CodeGenerator.cs @@ -101,7 +101,7 @@ private static string GeneratePropertiesCode(List commandProper properties.AppendLine(); foreach (var property in commandProperties) { - var type = GetTypeCode(property.Type); + var type = GetTypeCode(property.Type, property.IsRequired); properties.AppendLine($"public readonly {type} {property.CodeProperty};"); } @@ -114,39 +114,82 @@ private static string GenerateCreateMethodTempVariables(List co properties.AppendLine(); foreach (var property in commandProperties) { - var type = GetTypeCode(property.Type); - if (property.Type is CommandPropertyType.CommandId) - { - properties.AppendLine($"var {property.CodeProperty} = (CommandId)((int)json[\"{property.Name}\"]);"); - } - else if (property.Type is CommandPropertyType.Vector2) - { - properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); - properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector2((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1]);"); - } - else if (property.Type is CommandPropertyType.Vector3) - { - properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); - properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector3((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1], (float){property.CodeProperty}Array[2]);"); - } - else if (property.Type is CommandPropertyType.Vector4) - { - properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); - properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector4((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1], (float){property.CodeProperty}Array[2], (float){property.CodeProperty}Array[3]);"); - } - else if (property.Type is CommandPropertyType.Vector2Int) - { - properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); - properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector2Int((int){property.CodeProperty}Array[0], (int){property.CodeProperty}Array[1]);"); - } - else if (property.Type is CommandPropertyType.Vector3Int) + var type = GetTypeCode(property.Type, property.IsRequired); + + if (property.IsRequired) { - properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); - properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector3Int((int){property.CodeProperty}Array[0], (int){property.CodeProperty}Array[1], (int){property.CodeProperty}Array[2]);"); + // required の場合は従来通り + if (property.Type is CommandPropertyType.CommandId) + { + properties.AppendLine($"var {property.CodeProperty} = (CommandId)((int)json[\"{property.Name}\"]);"); + } + else if (property.Type is CommandPropertyType.Vector2) + { + properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); + properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector2((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1]);"); + } + else if (property.Type is CommandPropertyType.Vector3) + { + properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); + properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector3((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1], (float){property.CodeProperty}Array[2]);"); + } + else if (property.Type is CommandPropertyType.Vector4) + { + properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); + properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector4((float){property.CodeProperty}Array[0], (float){property.CodeProperty}Array[1], (float){property.CodeProperty}Array[2], (float){property.CodeProperty}Array[3]);"); + } + else if (property.Type is CommandPropertyType.Vector2Int) + { + properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); + properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector2Int((int){property.CodeProperty}Array[0], (int){property.CodeProperty}Array[1]);"); + } + else if (property.Type is CommandPropertyType.Vector3Int) + { + properties.AppendLine($"var {property.CodeProperty}Array = json[\"{property.Name}\"];"); + properties.AppendLine($"var {property.CodeProperty} = new global::UnityEngine.Vector3Int((int){property.CodeProperty}Array[0], (int){property.CodeProperty}Array[1], (int){property.CodeProperty}Array[2]);"); + } + else + { + properties.AppendLine($"var {property.CodeProperty} = ({type})json[\"{property.Name}\"];"); + } } else { - properties.AppendLine($"var {property.CodeProperty} = ({type})json[\"{property.Name}\"];"); + // nullable の場合はnullチェックを追加 + properties.AppendLine($"var {property.CodeProperty}Token = json[\"{property.Name}\"];"); + + if (property.Type is CommandPropertyType.CommandId) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : (CommandId?)((int){property.CodeProperty}Token);"); + } + else if (property.Type is CommandPropertyType.Vector2) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : new global::UnityEngine.Vector2((float){property.CodeProperty}Token[0], (float){property.CodeProperty}Token[1]);"); + } + else if (property.Type is CommandPropertyType.Vector3) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : new global::UnityEngine.Vector3((float){property.CodeProperty}Token[0], (float){property.CodeProperty}Token[1], (float){property.CodeProperty}Token[2]);"); + } + else if (property.Type is CommandPropertyType.Vector4) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : new global::UnityEngine.Vector4((float){property.CodeProperty}Token[0], (float){property.CodeProperty}Token[1], (float){property.CodeProperty}Token[2], (float){property.CodeProperty}Token[3]);"); + } + else if (property.Type is CommandPropertyType.Vector2Int) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : new global::UnityEngine.Vector2Int((int){property.CodeProperty}Token[0], (int){property.CodeProperty}Token[1]);"); + } + else if (property.Type is CommandPropertyType.Vector3Int) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : new global::UnityEngine.Vector3Int((int){property.CodeProperty}Token[0], (int){property.CodeProperty}Token[1], (int){property.CodeProperty}Token[2]);"); + } + else if (property.Type is CommandPropertyType.String) + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : (string?){property.CodeProperty}Token;"); + } + else + { + properties.AppendLine($"{type} {property.CodeProperty} = {property.CodeProperty}Token?.Type == global::Newtonsoft.Json.Linq.JTokenType.Null ? null : ({type}){property.CodeProperty}Token;"); + } } } @@ -169,7 +212,7 @@ public static string GenerateConstructorPropertiesCode(List com var properties = new StringBuilder(); foreach (var property in commandProperties) { - var type = GetTypeCode(property.Type); + var type = GetTypeCode(property.Type, property.IsRequired); properties.Append($", {type} {property.CodeProperty}"); } @@ -189,9 +232,9 @@ private static string GenerateConstructSetPropertiesCode(List c return construct.ToString(); } - private static string GetTypeCode(CommandPropertyType type) + private static string GetTypeCode(CommandPropertyType type, bool isRequired) { - return type switch + var baseType = type switch { CommandPropertyType.String => "string", CommandPropertyType.Int => "int", @@ -205,6 +248,20 @@ private static string GetTypeCode(CommandPropertyType type) CommandPropertyType.Vector3Int => "global::UnityEngine.Vector3Int", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; + + // 値型でrequiredでない場合は nullable にする + if (!isRequired && type != CommandPropertyType.String) + { + return baseType + "?"; + } + + // 参照型でも明示的に nullable にする(C# 8.0以降) + if (!isRequired && type == CommandPropertyType.String) + { + return baseType + "?"; + } + + return baseType; } public static string GenerateLoaderCode(CommandsSemantics commandsSemantics) diff --git a/CommandForgeGenerator/Semantic/CommandSemanticsLoader.cs b/CommandForgeGenerator/Semantic/CommandSemanticsLoader.cs index 89f39d5..cd8cdda 100644 --- a/CommandForgeGenerator/Semantic/CommandSemanticsLoader.cs +++ b/CommandForgeGenerator/Semantic/CommandSemanticsLoader.cs @@ -90,7 +90,14 @@ CommandsSemantics ParseCommandsSchema(JsonObject root) _ => throw new Exception($"未知の property type \"{typeStr}\"") }; - properties.Add(new CommandProperty(mappedType, propName)); + // required フィールドを読み取る + var isRequired = true; + if (propObj.Nodes.ContainsKey("required") && propObj["required"] is JsonBoolean requiredNode) + { + isRequired = requiredNode.Literal; + } + + properties.Add(new CommandProperty(mappedType, propName, isRequired)); } commands.Add(new CommandSemantics(commandName, properties)); diff --git a/CommandForgeGenerator/Semantic/CommandsSemantics.cs b/CommandForgeGenerator/Semantic/CommandsSemantics.cs index 3c2e413..1fa7c4b 100644 --- a/CommandForgeGenerator/Semantic/CommandsSemantics.cs +++ b/CommandForgeGenerator/Semantic/CommandsSemantics.cs @@ -28,13 +28,15 @@ public CommandSemantics(string name, List properties) public class CommandProperty{ public readonly string Name; public readonly CommandPropertyType Type; + public readonly bool IsRequired; public string CodeProperty => Name.ToUpper(0); - public CommandProperty(CommandPropertyType type, string name) + public CommandProperty(CommandPropertyType type, string name, bool isRequired) { Type = type; Name = name; + IsRequired = isRequired; } } diff --git a/CommandForgeGenerator/Semantic/ReservedCommandSemantics.cs b/CommandForgeGenerator/Semantic/ReservedCommandSemantics.cs index 46727e7..0969cdd 100644 --- a/CommandForgeGenerator/Semantic/ReservedCommandSemantics.cs +++ b/CommandForgeGenerator/Semantic/ReservedCommandSemantics.cs @@ -8,8 +8,8 @@ public static List GetReservedCommand() { var groupStart = new CommandSemantics("group_start", new List() { - new(CommandPropertyType.String, "groupName"), - new(CommandPropertyType.Bool, "isCollapsed"), + new(CommandPropertyType.String, "groupName", true), + new(CommandPropertyType.Bool, "isCollapsed", true), }); var groupEnd = new CommandSemantics("group_end", new List()); return new List(){groupStart, groupEnd};