using System.Text.Json; namespace LanMountainDesktop.AirAppSdk; /// /// AirApp manifest (airapp.json). /// public sealed record AirAppManifest( string Id, string Name, string EntranceAssembly, string? Description = null, string? Author = null, string? Version = null, string? ApiVersion = null, AirAppRuntimeConfiguration? Runtime = null, IReadOnlyList? Components = null, IReadOnlyList? Windows = null, IReadOnlyList? Permissions = null, IReadOnlyList? SharedContracts = null) { private static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; /// /// Load manifest from file. /// public static AirAppManifest Load(string manifestPath) { ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath); using var stream = File.OpenRead(manifestPath); return Load(stream, manifestPath); } /// /// Load manifest from stream. /// public static AirAppManifest Load(Stream stream, string sourceName) { ArgumentNullException.ThrowIfNull(stream); ArgumentException.ThrowIfNullOrWhiteSpace(sourceName); var manifest = JsonSerializer.Deserialize(stream, SerializerOptions); if (manifest is null) { throw new InvalidOperationException($"Failed to deserialize AirApp manifest '{sourceName}'."); } return manifest.NormalizeAndValidate(sourceName); } /// /// Resolve entrance assembly path. /// public string ResolveEntranceAssemblyPath(string manifestPath) { ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath); if (Path.IsPathRooted(EntranceAssembly)) { return Path.GetFullPath(EntranceAssembly); } var manifestDirectory = Path.GetDirectoryName(Path.GetFullPath(manifestPath)) ?? throw new InvalidOperationException($"Failed to determine directory of '{manifestPath}'."); return Path.GetFullPath(Path.Combine(manifestDirectory, EntranceAssembly)); } /// /// Get runtime mode. /// public AirAppRuntimeMode RuntimeMode => AirAppRuntimeModes.TryParse(Runtime?.Mode, out var mode) ? mode : AirAppRuntimeMode.InProcess; private AirAppManifest NormalizeAndValidate(string manifestPath) { var normalizedRuntime = (Runtime ?? new AirAppRuntimeConfiguration()).NormalizeAndValidate(manifestPath); var normalized = this with { Id = RequireValue(Id, nameof(Id), manifestPath), Name = RequireValue(Name, nameof(Name), manifestPath), EntranceAssembly = RequireValue(EntranceAssembly, nameof(EntranceAssembly), manifestPath), Description = NormalizeOptionalValue(Description), Author = NormalizeOptionalValue(Author), Version = NormalizeOptionalValue(Version), ApiVersion = NormalizeOptionalValue(ApiVersion) ?? AirAppSdkInfo.ApiVersion, Runtime = normalizedRuntime, Components = Components ?? Array.Empty(), Windows = Windows ?? Array.Empty(), Permissions = Permissions ?? Array.Empty(), SharedContracts = SharedContracts ?? Array.Empty() }; // Validate API version if (!System.Version.TryParse(normalized.ApiVersion, out var requestedVersion)) { throw new InvalidOperationException( $"AirApp manifest '{manifestPath}' declares invalid API version '{normalized.ApiVersion}'."); } if (!System.Version.TryParse(AirAppSdkInfo.ApiVersion, out var currentVersion)) { throw new InvalidOperationException($"AirApp SDK API version '{AirAppSdkInfo.ApiVersion}' is invalid."); } if (requestedVersion.Major != currentVersion.Major) { throw new InvalidOperationException( $"AirApp '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " + $"but the host provides '{AirAppSdkInfo.ApiVersion}' (major {currentVersion.Major}). " + $"This host only supports v{currentVersion.Major}.x AirApps and rejects v{requestedVersion.Major}.x packages. " + $"Migrate the AirApp manifest and code to API {AirAppSdkInfo.ApiVersion}, then rebuild and republish."); } return normalized; } private static string RequireValue(string? value, string propertyName, string manifestPath) { var normalized = NormalizeOptionalValue(value); if (string.IsNullOrWhiteSpace(normalized)) { throw new InvalidOperationException( $"AirApp manifest '{manifestPath}' is missing required property '{propertyName}'."); } return normalized; } private static string? NormalizeOptionalValue(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } } /// /// Component declaration in manifest. /// public sealed record AirAppComponentManifest( string Id, string Name, int DefaultWidth = 2, int DefaultHeight = 2, string? Description = null, string? Category = null, string? IconKey = null); /// /// Window declaration in manifest. /// public sealed record AirAppWindowManifest( string Id, string Name, double DefaultWidth = 800, double DefaultHeight = 600, string? Description = null); /// /// Shared contract reference. /// public sealed record AirAppSharedContractReference( string Id, string Version); /// /// Runtime configuration. /// public sealed record AirAppRuntimeConfiguration { public string? Mode { get; init; } public IReadOnlyList? Capabilities { get; init; } internal AirAppRuntimeConfiguration NormalizeAndValidate(string manifestPath) { return this with { Mode = string.IsNullOrWhiteSpace(Mode) ? "in-process" : Mode.Trim().ToLowerInvariant(), Capabilities = Capabilities ?? Array.Empty() }; } }