Add custom decoder inits for various oci types that use omitempty (#347)

Many fields on the various OCI types use "omitempty" for encoding and
decoding the json representation in golang. This PR adds custom json
decoder functions to allow for behavior similar to "omitempty".

---------

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
This commit is contained in:
Kathryn Baldauf
2025-10-24 15:05:40 -07:00
committed by GitHub
parent 0297605ef8
commit a27fefbb59
2 changed files with 272 additions and 2 deletions
+126 -2
View File
@@ -61,6 +61,27 @@ public struct Spec: Codable, Sendable {
case root
case linux
}
public init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.hooks = try container.decodeIfPresent(Hook.self, forKey: .hooks)
self.process = try container.decodeIfPresent(Process.self, forKey: .process)
if let hostname = try container.decodeIfPresent(String.self, forKey: .hostname) {
self.hostname = hostname
}
if let domainname = try container.decodeIfPresent(String.self, forKey: .domainname) {
self.domainname = domainname
}
if let mounts = try container.decodeIfPresent([Mount].self, forKey: .mounts) {
self.mounts = mounts
}
self.annotations = try container.decodeIfPresent([String: String].self, forKey: .annotations)
self.root = try container.decodeIfPresent(Root.self, forKey: .root)
self.linux = try container.decodeIfPresent(Linux.self, forKey: .linux)
}
}
public struct Process: Codable, Sendable {
@@ -78,6 +99,22 @@ public struct Process: Codable, Sendable {
public var args: [String]
public var terminal: Bool
public enum CodingKeys: String, CodingKey {
case cwd
case env
case consoleSize
case selinuxLabel
case noNewPrivileges
case commandLine
case oomScoreAdj
case capabilities
case apparmorProfile
case user
case rlimits
case args
case terminal
}
public init(
args: [String] = [],
cwd: String = "/",
@@ -120,6 +157,40 @@ public struct Process: Codable, Sendable {
}()
self.init(args: args, cwd: cwd, env: env, user: user)
}
public init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.cwd = try container.decode(String.self, forKey: .cwd)
if let env = try container.decodeIfPresent([String].self, forKey: .env) {
self.env = env
}
self.consoleSize = try container.decodeIfPresent(Box.self, forKey: .consoleSize)
if let selinuxLabel = try container.decodeIfPresent(String.self, forKey: .selinuxLabel) {
self.selinuxLabel = selinuxLabel
}
if let noNewPrivileges = try container.decodeIfPresent(Bool.self, forKey: .noNewPrivileges) {
self.noNewPrivileges = noNewPrivileges
}
if let commandLine = try container.decodeIfPresent(String.self, forKey: .commandLine) {
self.commandLine = commandLine
}
self.oomScoreAdj = try container.decodeIfPresent(Int.self, forKey: .oomScoreAdj)
self.capabilities = try container.decodeIfPresent(LinuxCapabilities.self, forKey: .capabilities)
if let apparmorProfile = try container.decodeIfPresent(String.self, forKey: .apparmorProfile) {
self.apparmorProfile = apparmorProfile
}
self.user = try container.decode(User.self, forKey: .user)
if let rlimits = try container.decodeIfPresent([POSIXRlimit].self, forKey: .rlimits) {
self.rlimits = rlimits
}
if let args = try container.decodeIfPresent([String].self, forKey: .args) {
self.args = args
}
if let terminal = try container.decodeIfPresent(Bool.self, forKey: .terminal) {
self.terminal = terminal
}
}
}
public struct LinuxCapabilities: Codable, Sendable {
@@ -160,6 +231,14 @@ public struct User: Codable, Sendable {
public var additionalGids: [UInt32]
public var username: String
public enum CodingKeys: String, CodingKey {
case uid
case gid
case umask
case additionalGids
case username
}
public init(
uid: UInt32 = 0,
gid: UInt32 = 0,
@@ -173,16 +252,42 @@ public struct User: Codable, Sendable {
self.additionalGids = additionalGids
self.username = username
}
public init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uid = try container.decode(UInt32.self, forKey: .uid)
self.gid = try container.decode(UInt32.self, forKey: .gid)
self.umask = try container.decodeIfPresent(UInt32.self, forKey: .umask)
if let additionalGids = try container.decodeIfPresent([UInt32].self, forKey: .additionalGids) {
self.additionalGids = additionalGids
}
if let username = try container.decodeIfPresent(String.self, forKey: .username) {
self.username = username
}
}
}
public struct Root: Codable, Sendable {
public var path: String
public var readonly: Bool
public enum CodingKeys: String, CodingKey {
case path
case readonly
}
public init(path: String, readonly: Bool) {
self.path = path
self.readonly = readonly
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.path = try container.decode(String.self, forKey: .path)
self.readonly = try container.decodeIfPresent(Bool.self, forKey: .readonly) ?? false
}
}
public struct Mount: Codable, Sendable {
@@ -194,9 +299,18 @@ public struct Mount: Codable, Sendable {
public var uidMappings: [LinuxIDMapping]
public var gidMappings: [LinuxIDMapping]
public enum CodingKeys: String, CodingKey {
case type
case source
case destination
case options
case uidMappings
case gidMappings
}
public init(
type: String,
source: String,
type: String = "",
source: String = "",
destination: String,
options: [String] = [],
uidMappings: [LinuxIDMapping] = [],
@@ -209,6 +323,16 @@ public struct Mount: Codable, Sendable {
self.uidMappings = uidMappings
self.gidMappings = gidMappings
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? ""
self.source = try container.decodeIfPresent(String.self, forKey: .source) ?? ""
self.destination = try container.decode(String.self, forKey: .destination)
self.options = try container.decodeIfPresent([String].self, forKey: .options) ?? []
self.uidMappings = try container.decodeIfPresent([LinuxIDMapping].self, forKey: .uidMappings) ?? []
self.gidMappings = try container.decodeIfPresent([LinuxIDMapping].self, forKey: .gidMappings) ?? []
}
}
public struct Hook: Codable, Sendable {
@@ -0,0 +1,146 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationOCI
struct OCISpecTests {
@Test func minimalSpecDecode() throws {
let version = "1.2.3"
let minSpec =
"""
{
"ociVersion": "\(version)"
}
"""
guard let data = minSpec.data(using: .utf8) else {
Issue.record("test spec is not valid: \(minSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Spec.self, from: data)
#expect(decodedSpec.version == version)
#expect(decodedSpec.hooks == nil)
#expect(decodedSpec.process == nil)
#expect(decodedSpec.hostname == "")
#expect(decodedSpec.domainname == "")
#expect(decodedSpec.mounts.isEmpty)
#expect(decodedSpec.annotations == nil)
#expect(decodedSpec.root == nil)
#expect(decodedSpec.linux == nil)
}
@Test func minimalProcessSpecDecode() throws {
let cwd = "/test"
let minProcessSpec =
"""
{
"cwd": "\(cwd)",
"user": {
"uid": 10,
"gid": 11
}
}
"""
guard let data = minProcessSpec.data(using: .utf8) else {
Issue.record("test process spec is not valid: \(minProcessSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Process.self, from: data)
#expect(decodedSpec.cwd == cwd)
#expect(decodedSpec.env.isEmpty)
#expect(decodedSpec.consoleSize == nil)
#expect(decodedSpec.selinuxLabel == "")
#expect(decodedSpec.noNewPrivileges == false)
#expect(decodedSpec.commandLine == "")
#expect(decodedSpec.oomScoreAdj == nil)
#expect(decodedSpec.capabilities == nil)
#expect(decodedSpec.apparmorProfile == "")
#expect(decodedSpec.user.uid == 10)
#expect(decodedSpec.user.gid == 11)
#expect(decodedSpec.rlimits.isEmpty)
#expect(decodedSpec.terminal == false)
}
@Test func minimalUserSpecDecode() throws {
let minUserSpec =
"""
{
"uid": 10,
"gid": 11
}
"""
guard let data = minUserSpec.data(using: .utf8) else {
Issue.record("test user spec is not valid: \(minUserSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.User.self, from: data)
#expect(decodedSpec.uid == 10)
#expect(decodedSpec.gid == 11)
#expect(decodedSpec.umask == nil)
#expect(decodedSpec.additionalGids.isEmpty)
#expect(decodedSpec.username == "")
}
@Test func minimalRootSpecDecode() throws {
let path = "/testpath"
let minRootSpec =
"""
{
"path": "\(path)"
}
"""
guard let data = minRootSpec.data(using: .utf8) else {
Issue.record("test root spec is not valid: \(minRootSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Root.self, from: data)
#expect(decodedSpec.path == path)
#expect(decodedSpec.readonly == false)
}
@Test func minimalMountSpecDecode() throws {
let destination = "/testdest"
let minMountSpec =
"""
{
"destination": "\(destination)"
}
"""
guard let data = minMountSpec.data(using: .utf8) else {
Issue.record("test mount spec is not valid: \(minMountSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Mount.self, from: data)
#expect(decodedSpec.type == "")
#expect(decodedSpec.source == "")
#expect(decodedSpec.destination == destination)
#expect(decodedSpec.options.isEmpty)
#expect(decodedSpec.uidMappings.isEmpty)
#expect(decodedSpec.gidMappings.isEmpty)
}
}