diff --git a/cmd/flamenco-manager-poc/main.go b/cmd/flamenco-manager-poc/main.go index 28083f68..6d7dabb2 100644 --- a/cmd/flamenco-manager-poc/main.go +++ b/cmd/flamenco-manager-poc/main.go @@ -27,6 +27,7 @@ import ( "net/http" "time" + "github.com/benbjohnson/clock" oapi_middle "github.com/deepmap/oapi-codegen/pkg/middleware" "github.com/getkin/kin-openapi/openapi3filter" "github.com/labstack/echo/v4" @@ -64,7 +65,8 @@ func main() { log.Info().Str("port", port).Msg("listening") // Construct the services. - compiler, err := job_compilers.Load() + timeService := clock.New() + compiler, err := job_compilers.Load(timeService) if err != nil { log.Fatal().Err(err).Msg("error loading job compilers") } diff --git a/go.mod b/go.mod index c8861490..3f0669ac 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,18 @@ module gitlab.com/blender/flamenco-goja-test go 1.16 require ( + github.com/benbjohnson/clock v1.3.0 github.com/deepmap/oapi-codegen v1.9.0 github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 github.com/getkin/kin-openapi v0.88.0 - github.com/golang-migrate/migrate/v4 v4.15.1 // indirect + github.com/golang-migrate/migrate/v4 v4.15.1 github.com/google/uuid v1.3.0 github.com/labstack/echo/v4 v4.6.1 github.com/mattn/go-colorable v0.1.12 github.com/rs/zerolog v1.26.1 github.com/stretchr/testify v1.7.0 github.com/ziflex/lecho/v3 v3.1.0 - modernc.org/sqlite v1.14.4 // indirect + golang.org/x/net v0.0.0-20211013171255-e13a2654a71e + modernc.org/sqlite v1.14.4 ) diff --git a/go.sum b/go.sum index 7e946199..800547e9 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNE github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -313,6 +315,7 @@ github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d/go.mod h1:R9ET47fwRVRP github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 h1:tYwu/z8Y0NkkzGEh3z21mSWggMg4LwLRFucLS7TjARg= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -487,6 +490,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -677,10 +681,10 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= @@ -712,6 +716,7 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -1033,6 +1038,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -1101,7 +1107,6 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211013171255-e13a2654a71e h1:Xj+JO91noE97IN6F/7WZxzC5QE6yENAQPrwIYhW3bsA= golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1612,11 +1617,13 @@ modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzq modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4= modernc.org/ccgo/v3 v3.14.0 h1:Zr1Ny9+7r5yAiXpBdgp8XiXqkNA4ARrRphHGHVXeAp0= modernc.org/ccgo/v3 v3.14.0/go.mod h1:hBrkiBlUwvr5vV/ZH9YzXIp982jKE8Ek8tR1ytoAL6Q= +modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= @@ -1683,11 +1690,13 @@ modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= +modernc.org/tcl v1.10.0 h1:vux2MNFhSXYqD8Kq4Uc9RjWcgv2c7Atx3da3VpLPPEw= modernc.org/tcl v1.10.0/go.mod h1:WzWapmP/7dHVhFoyPpEaNSVTL8xtewhouN/cqSJ5A2s= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.2.21 h1:UO0ptLQLHM2eNistCE6ofm1fMKKXk0n0IkrQly3z5HA= modernc.org/z v1.2.21/go.mod h1:uXrObx4pGqXWIMliC5MiKuwAyMrltzwpteOFUP1PWCc= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/manager/job_compilers/job_compilers.go b/internal/manager/job_compilers/job_compilers.go index 0ce38fdb..94f91c6b 100644 --- a/internal/manager/job_compilers/job_compilers.go +++ b/internal/manager/job_compilers/job_compilers.go @@ -39,8 +39,9 @@ var ErrScriptIncomplete = errors.New("job compiler script incomplete") // Service contains job compilers defined in JavaScript. type Service struct { - compilers map[string]Compiler // Mapping from job type name to the job compiler of that type. - registry *require.Registry // Goja module registry. + compilers map[string]Compiler // Mapping from job type name to the job compiler of that type. + registry *require.Registry // Goja module registry. + timeService TimeService } type Compiler struct { @@ -57,9 +58,16 @@ type VM struct { // jobCompileFunc is a function that fills job.Tasks. type jobCompileFunc func(job *AuthoredJob) error -func Load() (*Service, error) { +// TimeService is a service that can tell the current time. +type TimeService interface { + Now() time.Time +} + +// Load returns a job compiler service with all JS files loaded. +func Load(ts TimeService) (*Service, error) { compiler := Service{ - compilers: map[string]Compiler{}, + compilers: map[string]Compiler{}, + timeService: ts, } if err := compiler.loadScripts(); err != nil { @@ -94,6 +102,7 @@ func (s *Service) Compile(ctx context.Context, sj api.SubmittedJob) (*AuthoredJo // Create an AuthoredJob from this SubmittedJob. aj := AuthoredJob{ JobID: uuid.New().String(), // Ignore the submitted ID. + Created: s.timeService.Now(), Name: sj.Name, JobType: sj.Type, Priority: sj.Priority, @@ -103,14 +112,13 @@ func (s *Service) Compile(ctx context.Context, sj api.SubmittedJob) (*AuthoredJo Metadata: make(JobMetadata), } if sj.Settings != nil { - for key, value := range *sj.Settings { + for key, value := range sj.Settings.AdditionalProperties { aj.Settings[key] = value } } if sj.Metadata != nil { - for key, value := range *sj.Metadata { - // TODO: make sure OpenAPI understands these keys can only be strings. - aj.Metadata[key] = value.(string) + for key, value := range sj.Metadata.AdditionalProperties { + aj.Metadata[key] = value } } diff --git a/internal/manager/job_compilers/job_compilers_test.go b/internal/manager/job_compilers/job_compilers_test.go new file mode 100644 index 00000000..db8cfa90 --- /dev/null +++ b/internal/manager/job_compilers/job_compilers_test.go @@ -0,0 +1,115 @@ +// Package job_compilers contains functionality to convert a Flamenco job +// definition into concrete tasks and commands to execute by Workers. +package job_compilers + +/* ***** BEGIN GPL LICENSE BLOCK ***** + * + * Original Code Copyright (C) 2022 Blender Foundation. + * + * This file is part of Flamenco. + * + * Flamenco is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Flamenco. If not, see . + * + * ***** END GPL LICENSE BLOCK ***** */ + +import ( + "context" + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/stretchr/testify/assert" + "gitlab.com/blender/flamenco-goja-test/pkg/api" +) + +func exampleSubmittedJob() api.SubmittedJob { + settings := api.JobSettings{ + AdditionalProperties: map[string]interface{}{ + "blender_cmd": "{blender}", + "chunk_size": 3, + "extract_audio": true, + "filepath": "/render/sf/jobs/scene123.blend", + "format": "PNG", + "fps": 24, + "frames": "1-10", + "images_or_video": "images", + "output_file_extension": ".png", + "render_output": "/render/sf/frames/scene123", + }} + metadata := api.JobMetadata{ + AdditionalProperties: map[string]string{ + "project": "Sprite Fright", + "user.email": "sybren@blender.org", + "user.name": "Sybren Stüvel", + }} + sj := api.SubmittedJob{ + Name: "3Д рендеринг", + Priority: 50, + Type: "simple-blender-render", + Settings: &settings, + Metadata: &metadata, + } + return sj +} + +func mockedClock(t *testing.T) clock.Clock { + c := clock.NewMock() + now, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + assert.NoError(t, err) + c.Set(now) + return c +} + +func TestSimpleBlenderRenderHappy(t *testing.T) { + c := mockedClock(t) + + s, err := Load(c) + assert.NoError(t, err) + + // Compiling a job should be really fast. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + sj := exampleSubmittedJob() + aj, err := s.Compile(ctx, sj) + if err != nil { + t.Logf("job compiler failed: %v", err) + t.FailNow() + } + assert.NotNil(t, aj) + if aj == nil { + // Don't bother with the rest of the test, it'll dereference a nil pointer anyway. + return + } + + // Properties should be copied as-is. + assert.Equal(t, sj.Name, aj.Name) + assert.Equal(t, sj.Type, aj.JobType) + assert.Equal(t, sj.Priority, aj.Priority) + assert.EqualValues(t, sj.Settings.AdditionalProperties, aj.Settings) + assert.EqualValues(t, sj.Metadata.AdditionalProperties, aj.Metadata) + + settings := sj.Settings.AdditionalProperties + + // Tasks should have been created to render the frames. + assert.Equal(t, 4, len(aj.Tasks)) + t0 := aj.Tasks[0] + assert.Equal(t, "render-1-3", t0.Name) + assert.Equal(t, 1, len(t0.Commands)) + assert.Equal(t, "blender-render", t0.Commands[0].Type) + assert.Equal(t, "{blender}", t0.Commands[0].Parameters["cmd"]) + assert.Equal(t, settings["filepath"], t0.Commands[0].Parameters["filepath"]) + assert.Equal(t, settings["format"], t0.Commands[0].Parameters["format"]) + assert.Equal(t, "1-3", t0.Commands[0].Parameters["frames"]) + assert.Equal(t, "/render/sf__intermediate-2006-01-02_090405/frames", t0.Commands[0].Parameters["render_output"]) +} diff --git a/internal/manager/job_compilers/js_globals.go b/internal/manager/job_compilers/js_globals.go index 834425d2..60653eff 100644 --- a/internal/manager/job_compilers/js_globals.go +++ b/internal/manager/job_compilers/js_globals.go @@ -21,16 +21,189 @@ package job_compilers * ***** END GPL LICENSE BLOCK ***** */ import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + "github.com/dop251/goja" "github.com/rs/zerolog/log" ) -func jsPrint(call goja.FunctionCall) goja.Value { +// ---------------------------------------------------------- +// Functions that start with `JS` are exposed to JavaScript. +// See newGojaVM() for the actual expose-as-globals code. +// ---------------------------------------------------------- + +func JSPrint(call goja.FunctionCall) goja.Value { log.Info().Interface("args", call.Arguments).Msg("print") return goja.Undefined() } -func jsAlert(call goja.FunctionCall) goja.Value { +func JSAlert(call goja.FunctionCall) goja.Value { log.Warn().Interface("args", call.Arguments).Msg("alert") return goja.Undefined() } + +// JSFormatTimestampLocal returns the timestamp formatted as local time in a way that's compatible with filenames. +func JSFormatTimestampLocal(timestamp time.Time) string { + return timestamp.Local().Format("2006-01-02_150405") +} + +type ErrInvalidRange struct { + Range string // The frame range that was invalid. + Message string // The error message + err error // Any wrapped error +} + +func (e ErrInvalidRange) Error() string { + if e.err != nil { + return fmt.Sprintf("invalid range \"%v\": %s (%s)", e.Range, e.Message, e.Error()) + } + return fmt.Sprintf("invalid range \"%v\": %s", e.Range, e.Message) +} + +func (e ErrInvalidRange) Unwrap() error { + return e.err +} + +func errInvalidRange(theRange, message string, errs ...error) error { + e := ErrInvalidRange{ + Range: theRange, + Message: message, + } + for _, err := range errs { + if err != nil { + e.err = err + break + } + } + return e +} + +const ( + chunkRegular = "-" + chunkBlender = ".." +) + +// JSFrameChunker takes a range like "1..10,20..25,40" and returns chunked ranges. +// +// The returned ranges will be at most `chunkSize` frames long. +// +// Supports "regular" and "blender" notation, resp. "A-Z" and "A..Z". Returned +// chunks will always be in "regular" notation because they're more compatible +// with embedding in filenames. +func JSFrameChunker(frameRange string, chunkSize int) ([]string, error) { + frameRange = strings.TrimSpace(frameRange) + if len(frameRange) == 0 { + return nil, errInvalidRange(frameRange, "empty range") + } + if chunkSize < 1 { + return nil, fmt.Errorf("invalid chunk size, must be positive number: %d", chunkSize) + } + + frames, err := frameRangeExplode(frameRange) + if err != nil { + return nil, err + } + if len(frames) == 0 { + return nil, errInvalidRange(frameRange, "empty range") + } + + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + var i int + chunks := make([]string, 0) + for i = 0; i < len(frames); i += chunkSize { + chunkFrames := frames[i:min(i+chunkSize, len(frames))] + chunkRange := frameRangeMerge(chunkFrames) + chunks = append(chunks, chunkRange) + } + + return chunks, nil +} + +// Given a range of frames, return an array containing each frame number. +func frameRangeExplode(frameRange string) ([]int, error) { + // Store as map to avoid duplicate frames. + frames := make(map[int]struct{}, 0) + + // Convert from "blender" to "regular" range notation. + frameRange = strings.ReplaceAll(frameRange, chunkBlender, chunkRegular) + + // parseInt first trims whitespace before converting to integer. + parseInt := func(s string) (int64, error) { + return strconv.ParseInt(strings.TrimSpace(s), 10, 64) + } + + // Explode each comma-separated frame range. + for _, part := range strings.Split(frameRange, ",") { + startEnd := strings.Split(part, chunkRegular) + switch len(startEnd) { + case 1: // Single frame + frame, err := parseInt(startEnd[0]) + if err != nil { + return nil, errInvalidRange(frameRange, part, err) + } + frames[int(frame)] = struct{}{} + case 2: // Frame range A-B + startFrame, startErr := parseInt(startEnd[0]) + endFrame, endErr := parseInt(startEnd[1]) + if startErr != nil || endErr != nil { + return nil, errInvalidRange(frameRange, part, startErr, endErr) + } + for frame := startFrame; frame <= endFrame; frame++ { + frames[int(frame)] = struct{}{} + } + default: + return nil, errInvalidRange(frameRange, part) + } + } + + // Convert from map to sorted array. + frameList := make([]int, 0, len(frames)) + for frame := range frames { + frameList = append(frameList, frame) + } + sort.Ints(frameList) + return frameList, nil +} + +// frameRangeMerge merges consecutive frames into ranges like "3..8,13,15..17". +func frameRangeMerge(frames []int) string { + startFrame := frames[0] + prevFrame := frames[0] + + ranges := make([]string, 0) + + appendRange := func(fromFrame, toFrame int) { + switch { + case fromFrame == toFrame: // Last range was one frame only + ranges = append(ranges, strconv.FormatInt(int64(fromFrame), 10)) + case fromFrame+1 == toFrame: // Last range was only two frames + ranges = append(ranges, strconv.FormatInt(int64(fromFrame), 10)) + ranges = append(ranges, strconv.FormatInt(int64(toFrame), 10)) + default: + ranges = append(ranges, fmt.Sprintf("%v%s%v", fromFrame, chunkRegular, toFrame)) + } + } + + var currentFrame int + for _, currentFrame = range frames { + if currentFrame > prevFrame+1 { + // This frame starts a new range, so append the one we now know ended. + appendRange(startFrame, prevFrame) + startFrame = currentFrame + } + prevFrame = currentFrame + } + appendRange(startFrame, currentFrame) + + return strings.Join(ranges, ",") +} diff --git a/internal/manager/job_compilers/js_globals_test.go b/internal/manager/job_compilers/js_globals_test.go new file mode 100644 index 00000000..a235ddd5 --- /dev/null +++ b/internal/manager/job_compilers/js_globals_test.go @@ -0,0 +1,70 @@ +package job_compilers + +/* ***** BEGIN GPL LICENSE BLOCK ***** + * + * Original Code Copyright (C) 2022 Blender Foundation. + * + * This file is part of Flamenco. + * + * Flamenco is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Flamenco. If not, see . + * + * ***** END GPL LICENSE BLOCK ***** */ + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFrameChunkerHappyBlenderStyle(t *testing.T) { + chunks, err := JSFrameChunker("1..10,20..25,40,3..8", 4) + assert.Nil(t, err) + assert.Equal(t, []string{"1-4", "5-8", "9,10,20,21", "22-25", "40"}, chunks) +} + +func TestFrameChunkerHappySmallInput(t *testing.T) { + // No frames, should be an error + chunks, err := JSFrameChunker(" ", 4) + assert.ErrorIs(t, err, ErrInvalidRange{Message: "empty range"}) + + // Just one frame. + chunks, err = JSFrameChunker("47", 4) + assert.Nil(t, err) + assert.Equal(t, []string{"47"}, chunks) + + // Just one range of exactly one chunk. + chunks, err = JSFrameChunker("1-3", 3) + assert.Nil(t, err) + assert.Equal(t, []string{"1-3"}, chunks) +} + +func TestFrameChunkerHappyRegularStyle(t *testing.T) { + chunks, err := JSFrameChunker("1-10,20-25,40", 4) + assert.Nil(t, err) + assert.Equal(t, []string{"1-4", "5-8", "9,10,20,21", "22-25", "40"}, chunks) +} + +func TestFrameChunkerHappyExtraWhitespace(t *testing.T) { + chunks, err := JSFrameChunker(" 1 .. 10,\t20..25\n,40 ", 4) + assert.Nil(t, err) + assert.Equal(t, []string{"1-4", "5-8", "9,10,20,21", "22-25", "40"}, chunks) +} + +func TestFrameRangeExplode(t *testing.T) { + frames, err := frameRangeExplode("1..10,20..25,40") + assert.Nil(t, err) + assert.Equal(t, []int{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 20, 21, 22, 23, 24, 25, 40, + }, frames) +} diff --git a/internal/manager/job_compilers/scripts.go b/internal/manager/job_compilers/scripts.go index b16bc374..4b467c39 100644 --- a/internal/manager/job_compilers/scripts.go +++ b/internal/manager/job_compilers/scripts.go @@ -96,9 +96,11 @@ func (s *Service) newGojaVM() *goja.Runtime { } } - // Set some global functions for script debugging purposes. - mustSet("print", jsPrint) - mustSet("alert", jsAlert) + // Set some global functions. + mustSet("print", JSPrint) + mustSet("alert", JSAlert) + mustSet("frameChunker", JSFrameChunker) + mustSet("formatTimestampLocal", JSFormatTimestampLocal) // Pre-import some useful modules. s.registry.Enable(vm) diff --git a/internal/manager/job_compilers/scripts/simple_blender_render.js b/internal/manager/job_compilers/scripts/simple_blender_render.js index 6c9d60be..3c118b4f 100644 --- a/internal/manager/job_compilers/scripts/simple_blender_render.js +++ b/internal/manager/job_compilers/scripts/simple_blender_render.js @@ -81,20 +81,14 @@ function compileJob(job) { // Determine the intermediate render output path. function intermediatePath(job, finalDir) { const basename = path.basename(finalDir); - const name = `${basename}__intermediate-${job.created}`; + const name = `${basename}__intermediate-${formatTimestampLocal(job.created)}`; return path.join(path.dirname(finalDir), name); } -function frameChunker(frames, callback) { - // TODO: actually implement. - callback("1-10"); - callback("11-20"); - callback("21-30"); -} - function authorRenderTasks(settings, renderDir, renderOutput) { let renderTasks = []; - frameChunker(settings.frames, function(chunk) { + let chunks = frameChunker(settings.frames, settings.chunk_size); + for (let chunk of chunks) { const task = author.Task(`render-${chunk}`); const command = author.Command("blender-render", { cmd: settings.blender_cmd, @@ -105,7 +99,7 @@ function authorRenderTasks(settings, renderDir, renderOutput) { }); task.addCommand(command); renderTasks.push(task); - }); + } return renderTasks; } diff --git a/internal/manager/persistence/db.go b/internal/manager/persistence/db.go index 885044c3..b4f36a39 100644 --- a/internal/manager/persistence/db.go +++ b/internal/manager/persistence/db.go @@ -114,10 +114,7 @@ func (db *DB) FetchJob(ctx context.Context, jobID string) (*api.Job, error) { return nil, err } - // TODO: make settings and metadata an explicit type. - settings := make(map[string]interface{}) - metadata := make(map[string]interface{}) - + var settings api.JobSettings rows, err := db.sqldb.QueryContext(ctx, "SELECT key, value FROM job_settings WHERE job_id=?", jobID) if err != nil { return nil, err @@ -128,7 +125,7 @@ func (db *DB) FetchJob(ctx context.Context, jobID string) (*api.Job, error) { if err := rows.Scan(&key, &value); err != nil { return nil, err } - settings[key] = value + settings.AdditionalProperties[key] = value } if err := rows.Close(); err != nil { return nil, err @@ -137,6 +134,7 @@ func (db *DB) FetchJob(ctx context.Context, jobID string) (*api.Job, error) { return nil, err } + var metadata api.JobMetadata rows, err = db.sqldb.QueryContext(ctx, "SELECT key, value FROM job_metadata WHERE job_id=?", jobID) if err != nil { return nil, err @@ -147,7 +145,7 @@ func (db *DB) FetchJob(ctx context.Context, jobID string) (*api.Job, error) { if err := rows.Scan(&key, &value); err != nil { return nil, err } - metadata[key] = value + metadata.AdditionalProperties[key] = value } if err := rows.Close(); err != nil { return nil, err diff --git a/pkg/api/flamenco-manager.yaml b/pkg/api/flamenco-manager.yaml index 4dfbd330..de6ec446 100644 --- a/pkg/api/flamenco-manager.yaml +++ b/pkg/api/flamenco-manager.yaml @@ -261,12 +261,9 @@ components: properties: "name": {type: string} "type": {type: string} - "status": {$ref: "#/components/schemas/JobStatus"} "priority": {type: integer, default: 50} - "settings": - type: object - "metadata": - type: object + "settings": {$ref: "#/components/schemas/JobSettings"} + "metadata": {$ref: "#/components/schemas/JobMetadata"} required: [name, type, priority] example: type: "simple-blender-render" @@ -286,7 +283,7 @@ components: metadata: "user.name": "Sybren Stüvel" "user.email": "sybren@blender.org" - project: "Sprite Fright" + "project": "Sprite Fright" Job: allOf: - $ref: '#/components/schemas/SubmittedJob' @@ -303,8 +300,22 @@ components: type: string format: date-time description: Creation timestamp - required: [id, created, updated] + status: {$ref: "#/components/schemas/JobStatus"} + required: [id, created, updated, status] + JobSettings: + type: object + additionalProperties: true + + JobMetadata: + type: object + description: Arbitrary metadata strings. More complex structures can be modeled by using `a.b.c` notation for the key. + additionalProperties: + type: string + example: + "user.name": Sybren Stüvel + "user.email": sybren@blender.org + "project": "Sprite Fright" Error: type: object diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go index 593e2680..7d4de50d 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -18,48 +18,50 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "+iN+cj7sCKYt+aj3/nL/3wAAAP//aU95WLUhAAA=", + "H4sIAAAAAAAC/7xa224bN/p/FYL9A/8WO5KcuHujq02bJnXQg1G56EViyJzhJw1tDjkhOZK1gYA+xL7J", + "boG92F7tC7hvtPhIzkkztpzdHBAYoyH58Tv+voP0jma6KLUC5Sydv6M2y6Fg/vGZtWKtgF8we4OfOdjM", + "iNIJrei8t0qEJYw4fGKWCIefDWQgNsBJuiMuB/KLNjdgpjShpdElGCfA35LpomCK+2fhoPAP/2dgRef0", + "s1nL3CxyNvs6HKD7hLpdCXROmTFsh58Fx8MrbQrm6JxWleC02WWdEWqN2651ivvG3i9LI7QRbtfZIJSD", + "NZh6R3g7clyxYnzhYZrWMVcdlRrVvAg7UXBmb+5npLJgRhb2CTXwthIGOJ2/pl43qIp4IArQMNTh+0A1", + "HT10WUlaW142WtfpNWQOuXq2YUKyVMIrnS7AOeRp4FULodYSiA3rRK8II690SpCaHXGeXIssPPbp/JKD", + "ImuxAZUQKQrhvA9umBQc/1ZgidP4zgKJRKbkRyV3pLLII9kKl5OgOX853t2450Djh47IYcUq6YZ8XeRA", + "4mLgg9hcb1VkhqAhyBZ55+DAFEL5+3Nha5VMkTxw4ZDLQD9etWLSQjLUg8vBIH0mpd4SPHpIk7CVwz05", + "kGudkpxZkgIoYqu0EM4Bn5JfdCU5EUUpd4SDhHBMSgK3wgaCzN5YstImkL7WaUKY4ogDuiiFxD3CTd+o", + "NiJTrSUwhRLdwG6orDMOyomVABPpNo6RkKKyjqRAKiXeVsFcQjUi1BYbGKoNgPfQnCgK4II5kDtiAP2Z", + "MH8Nh5VQAg8k6KpecLwy8fzoyoVXJTNOZJVkprHiPWqwVVpH9UNgMBJKi3iyccb3pnARj2+EFYe+5Uz1", + "kILQh/seFW3x81kIYVRW7U2GfC7FDRBGvpKgOBjCOJ9o9cWULMAhuStvkKsQCCGbMEUQMo1isrnD5czh", + "1ZXk6v+9MzSxBIr7WLLjij5AQnS+uOmRwLVo7XSAX1U6wZXgDsEZa5uTrytjQDm5IxqRhtV0vXd3sMZO", + "ydW3zxbffvN8+eLsu2+W588uvr0KOZYLA5nTZkdK5nLyJ3L1hs4+8//e0CvCyhJVyoPYoKoC5VsJCUvc", + "TxPKhakf/euI+TmzOfBlu/NyJHjuc5ohykUNdKTvRGwAWGbJ2fPzgOY7LzY6TXSJKflBEwXWAUfFVJmr", + "DFjyuQdYmxAuMryKGQH2C8IMEFuVpTbuUPTIfIIJ9/QpCi01czTxvnBUyHHp6nzU3hlqHGHJ90yxNZiA", + "fML50GcFQvlI8pIsBfl+lURU5uOLpbGkO8hXB+EQXSKw17nzWGygtkZS8XfCutoZvHffr7ehjupC47+T", + "+KKHiPeI214xJmBdbQ7EigvEQGnAIguEERvKl1gHeSS6haxycKwKfpTFD5gbN9uD5vrGGO1Lw8ManEOv", + "cq6jZVitFmAtW8Px8tLTbPePcfMq1OFMyh9XdP76Ybsu6mIET+2TgQgGmIMxO+GC0Io4UYB1rCgRBWpB", + "OXMwwZWxYkGMkPv557PnNbi/8sXz0XbjcQU+BmhT31cl/8DSjBX/tc7a+xpmL/eXwUDfg2OcOeYNxbkv", + "dpg87+l+IPFBp2hS4QwzO1JEYjHZ2Sn5XhsfLqWE2y7SZ0xhrig0FpseJyqMLXLFpuk0uyJKu6CHujC8", + "gR1GFdwypBVd3DvanC5KIxyQF0ascxfbnSkUTEjkepcaUH9JY+LRZl3vCDFJF34DWbh//2sDsgMnPUde", + "dOJ0XE+hhho92zhInbZY5sTGd1RMZaiB0FyVElx8VkFZQqvJiomwo3koWWX9w9sKKv/ATJZjN948hqwY", + "yE/QM3yyjUR6L/xzoFKhiibdy2lCt8x3FJOVNhOsH+xoWv0J1sI6MMADBA5BiHFuwI471CMbe8msW3rd", + "9bvtTmYV2c39fbpkDu8YB2K9cltm7kHpxoLDpTrZLZtOuZ/MjjSTY4HbSJE0Wuu27bUYCc1Cxemvpof6", + "6ch0D5tjoL2ArDLC7e5JJY/ODw8lhh7Wj9ZfbefVdqmYbl9IVoDK9AEWFB0U+3i4EBdO7/5G/vj17re7", + "3+/+cffbH7/e/fPu97u/d+cp8z+f9DN7vGWZFZzO6bv4cY8WzCt1s7Tir0DnpyiTMyxzS1ZxoWtMwajz", + "Rfuczow/ObOr2bVOMbuAgidPT6eeZDdXnP/wEj+Wls6ffpnQFdapls7pk8mTE6yXC7YGu9RmuREcNBYB", + "/g1NqK5cWbnQK8CtA2WDXaalx5TAwTLs6rMULmmY6ji7FWiqSRR8Eo6E2Vnfu1o7HkmmTeJ67GSuaXbR", + "OCNjuo65juXxemunGX84GGIwxzFaw9VYbHQGge+RMJrU0GA5xn6bOh6TCJqsUhqdgcV8PAr1AeAD4BsW", + "gvYQJv4HHIbMgPsUUBtv6gHq6BUdQB5aLLDsIXOBLhJY2nodLVmFUTuoMS0YpIYtP9Y2YTM5e56Qklm7", + "1YbXS4HHMOkkzNVbTUf5CIXeOf0IhFmRtakzd66ke+RRqJUOrYByLHNtT0JrSCUXwFAFlZHxpJ3PZqsa", + "cIWeDUu/n8KA5wUzBSlCj0eenZ9hKhIZKAude16ef7c5HdDfbrfTtaoQf2fxjJ2tSzk5nZ5MQU1zV4Sa", + "TDjZ4zZeRxO6ARMR6sn0ZHqCu3UJipUCwdq/Qud2ubfMjJXCY6d3Wm29KtB1vTLPeBjyFMKF6j8GyFea", + "72r1gfJnWFlKkflTs2sbgiCAxDEI6bc6+4FW/QBCx8RHu06L+cB7sS01agpvenpy8kk52zLs8LMM7KqS", + "ckfC+Bc4EcppIhQXG8ErJsPEeHowLv8gbIbaZIQ/v0Dq0sPHZlUUzOwaqxJGFGz9sAJbi8ad4oSi09L7", + "+TLDhOBnCNgzdcm9qkeeFp2PgOKlFsp5eRsfmzUYtYYRR3sJrpmrfESrDoc4I6prNrWDnAMFvgRH5GDY", + "4+cgOQhzMAt7QHXtVY36r9vvgHr6e3et06Xg+3tV+AJclodQbe/3wwaBUsVRaISgQGwQUUlHj0d6kP3l", + "R7TTA0Hn4btvDi+5XyAsDd9FeNs9wm/DIcUjiBbIea32kGFmJvZzk23bzo2CZd34xbbv4yDmSMkxoqiw", + "C0O45v6TguegBR5hUaF7SVLz8EnBsVJwW0KGnRTEPV3HqNmPCLmt7Vn7UnxxOXIomARxoT1pDz3KxZ8Z", + "3JNzsxx4JeEidLIfDwu7P3oYUZL/uUM3CewT+vTky2ER94OOX4n2v+bx4+56CrxP6Jcnpx8uO/da8xHm", + "z8HU+eg5KAG8V556VOwVpq8vEc9aa/6YOiZUdADX18QxT/CKs9GKppsPPQtmU+NyqP9mFK+OFA91+xXq", + "E/97lfov+pCTNTifKJoxY8pkKlkP362fHR+ktvOzfrIP9vE0M10UlUJ7xN8EHFYE05Z8lHt/uf9PAAAA", + "///WNQTcViMAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/api/openapi_types.gen.go b/pkg/api/openapi_types.gen.go index c47e1da4..53c8eb02 100644 --- a/pkg/api/openapi_types.gen.go +++ b/pkg/api/openapi_types.gen.go @@ -4,6 +4,8 @@ package api import ( + "encoding/json" + "fmt" "time" ) @@ -167,12 +169,23 @@ type Job struct { Created time.Time `json:"created"` // UUID of the Job - Id string `json:"id"` + Id string `json:"id"` + Status JobStatus `json:"status"` // Creation timestamp Updated time.Time `json:"updated"` } +// Arbitrary metadata strings. More complex structures can be modeled by using `a.b.c` notation for the key. +type JobMetadata struct { + AdditionalProperties map[string]string `json:"-"` +} + +// JobSettings defines model for JobSettings. +type JobSettings struct { + AdditionalProperties map[string]interface{} `json:"-"` +} + // JobStatus defines model for JobStatus. type JobStatus string @@ -195,12 +208,12 @@ type SecurityError struct { // Job definition submitted to Flamenco. type SubmittedJob struct { - Metadata *map[string]interface{} `json:"metadata,omitempty"` - Name string `json:"name"` - Priority int `json:"priority"` - Settings *map[string]interface{} `json:"settings,omitempty"` - Status *JobStatus `json:"status,omitempty"` - Type string `json:"type"` + // Arbitrary metadata strings. More complex structures can be modeled by using `a.b.c` notation for the key. + Metadata *JobMetadata `json:"metadata,omitempty"` + Name string `json:"name"` + Priority int `json:"priority"` + Settings *JobSettings `json:"settings,omitempty"` + Type string `json:"type"` } // TaskStatus defines model for TaskStatus. @@ -225,3 +238,109 @@ type SubmitJobJSONRequestBody SubmitJobJSONBody // RegisterWorkerJSONRequestBody defines body for RegisterWorker for application/json ContentType. type RegisterWorkerJSONRequestBody RegisterWorkerJSONBody + +// Getter for additional properties for JobMetadata. Returns the specified +// element and whether it was found +func (a JobMetadata) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for JobMetadata +func (a *JobMetadata) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for JobMetadata to handle AdditionalProperties +func (a *JobMetadata) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for JobMetadata to handle AdditionalProperties +func (a JobMetadata) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for JobSettings. Returns the specified +// element and whether it was found +func (a JobSettings) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for JobSettings +func (a *JobSettings) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for JobSettings to handle AdditionalProperties +func (a *JobSettings) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for JobSettings to handle AdditionalProperties +func (a JobSettings) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +}