feat(foundation): preview, diagnostic and render target ports with registration hooks (P6a)
The contracts the preview, bridge and render target packages plug into. No label prints differently and no existing host adapter changes. GenerationPorts: - Diagnostic (with Severity): a code from a closed vocabulary, an author element id, a box and named integers. It has no field that holds free text. - PreviewRequest, PreviewPage and Preview, and IsolatedRenderer.preview as a default that refuses, so an adapter written before it compiles unchanged. - 15 version 2 FailureCode values after the 13 released ones, and RenderException(FailureCode, Diagnostic) beside the existing constructor. - Integer targetDpi on RenderRequest (absent, 203, 300 or 600); the seven-argument constructor is kept and nothing reads the value yet. Registration points, each an empty class that is already called: GenerationPreviewProcesses, GenerationAssetProcesses, GenerationUpgradeProcesses and GenerationAssetTables from the producer, and GenerationPreviewWidget.section(table) from GenerationTables. Bridge: IsolatedRendererBridge maps any FailureCode name from a 0.1.0 worker to a typed refusal, so growing the enum alone would have widened what such a worker can name. The 13 released codes are now the set a 0.1.0 worker may name; BridgeTest asserts the other 15 stay generic. Tests: record validation, default refusals, source compatibility of the existing constructors, and that the producer registers exactly the tables, processes, choices, widgets and template sections it did before (the lists were captured from the untouched v1.1.0-RC.2 tag). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ba676106f
commit
c6aa0bf6b0
12 files changed
+906
-4
No files matched your search
+11
@@ -43,6 +43,13 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
*******************************************************************************/
|
||||
public final class IsolatedRendererBridge implements GenerationPorts.IsolatedRenderer
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the refusals renderer 0.1.0 can name. FailureCode also holds the version 2 scene refusals; a 0.1.0 worker //
|
||||
// cannot produce those, so one that names them is rejected like any other unknown code //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final Set<GenerationPorts.FailureCode> RENDERER_0_1_REFUSALS = java.util.EnumSet.range(
|
||||
GenerationPorts.FailureCode.BARCODE_FIT_TOO_SMALL, GenerationPorts.FailureCode.UNSUPPORTED_BARCODE_OPTION);
|
||||
|
||||
private final RendererLimits config;
|
||||
private final AssetReader assets;
|
||||
private final Exchange exchange;
|
||||
@@ -268,6 +275,10 @@ public final class IsolatedRendererBridge implements GenerationPorts.IsolatedRen
|
||||
{
|
||||
throw new IllegalArgumentException("WORKER_ERROR_REJECTED");
|
||||
}
|
||||
if(!RENDERER_0_1_REFUSALS.contains(code))
|
||||
{
|
||||
throw new IllegalArgumentException("WORKER_ERROR_REJECTED");
|
||||
}
|
||||
throw new GenerationPorts.RenderException(code);
|
||||
}
|
||||
int expectedFields = operation.equals("VALIDATE") ? 5 : 10;
|
||||
|
||||
+13
-1
@@ -183,13 +183,25 @@ class BridgeTest
|
||||
@Test
|
||||
void exactWorkerErrorsPreserveClosedFailureCodes() throws Exception
|
||||
{
|
||||
assertEquals(13, GenerationPorts.FailureCode.values().length);
|
||||
var released = java.util.EnumSet.range(GenerationPorts.FailureCode.BARCODE_FIT_TOO_SMALL, GenerationPorts.FailureCode.UNSUPPORTED_BARCODE_OPTION);
|
||||
assertEquals(13, released.size());
|
||||
assertEquals(28, GenerationPorts.FailureCode.values().length);
|
||||
for(var code : GenerationPorts.FailureCode.values())
|
||||
{
|
||||
var bridge = new IsolatedRendererBridge(config(directory), (actor, asset, maximum) -> new byte[0], (id, input, maximum) ->
|
||||
(RendererJson.JSON.writeValueAsString(Map.of("protocol", RendererJson.PROTOCOL, "requestId", id, "outcome", "ERROR",
|
||||
"rendererVersion", "qqq-renderer-0.1.0", "catalogVersion", "qqq-barcodes-9", "code", code.name())) + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
var request = request(Map.of(), List.of());
|
||||
if(!released.contains(code))
|
||||
{
|
||||
// A version 2 scene refusal named by a 0.1.0 worker stays as unknown as it was before the vocabulary grew.
|
||||
for(var generic : List.of(assertThrows(QException.class, () -> bridge.render(request)), assertThrows(QException.class, () -> bridge.validate("actor-1", request.revision()))))
|
||||
{
|
||||
assertEquals(QException.class, generic.getClass(), code.name());
|
||||
assertEquals("Isolated renderer rejected or could not complete the request.", generic.getMessage());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var failure = assertThrows(GenerationPorts.RenderException.class, () -> bridge.render(request));
|
||||
assertEquals(code, failure.code());
|
||||
assertEquals(code.name(), failure.getMessage());
|
||||
|
||||
+5
-1
@@ -24,6 +24,7 @@ package com.kingsrook.qbits.documentgeneration;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@@ -138,7 +139,7 @@ public class DocumentGenerationQBitProducer implements QBitMetaDataProducer<Docu
|
||||
QBitMetaData bit = getQBitMetaData();
|
||||
instance.addQBit(bit);
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
GenerationTables.tables(config).forEach(table ->
|
||||
Stream.concat(GenerationTables.tables(config).stream(), GenerationAssetTables.tables(config).stream()).forEach(table ->
|
||||
{
|
||||
table.setSourceQBitName(bit.getName());
|
||||
output.add(table);
|
||||
@@ -146,6 +147,9 @@ public class DocumentGenerationQBitProducer implements QBitMetaDataProducer<Docu
|
||||
output.addSelfToInstance(instance);
|
||||
GenerationProcesses.register(instance, config.getSourceTables());
|
||||
GenerationAuthoringProcesses.register(instance, config);
|
||||
GenerationPreviewProcesses.register(instance, config);
|
||||
GenerationAssetProcesses.register(instance, config);
|
||||
GenerationUpgradeProcesses.register(instance, config);
|
||||
return (new MetaDataProducerMultiOutput());
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
/*******************************************************************************
|
||||
** Processes that upload and manage built-in template assets such as fonts.
|
||||
** The producer already calls this registration point; it registers nothing yet.
|
||||
*******************************************************************************/
|
||||
final class GenerationAssetProcesses
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Metadata namespace.
|
||||
*******************************************************************************/
|
||||
private GenerationAssetProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once by the producer after the generation and authoring processes.
|
||||
*******************************************************************************/
|
||||
static void register(QInstance instance, DocumentGenerationQBitConfig config) throws QException
|
||||
{
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** Tables of the built-in asset store. The producer already registers whatever
|
||||
** this returns, with the QBit's own tables; it returns none yet.
|
||||
*******************************************************************************/
|
||||
final class GenerationAssetTables
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Metadata namespace.
|
||||
*******************************************************************************/
|
||||
private GenerationAssetTables()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Complete table metadata, on the configured backend, for the producer to add.
|
||||
*******************************************************************************/
|
||||
static List<QTableMetaData> tables(DocumentGenerationQBitConfig config)
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
}
|
||||
+230
-2
@@ -38,6 +38,17 @@ public final class GenerationPorts
|
||||
{
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A render target is absent or one of the resolutions the renderer supports.
|
||||
***************************************************************************/
|
||||
private static void renderTarget(Integer dpi)
|
||||
{
|
||||
if(dpi != null && !List.of(203, 300, 600).contains(dpi))
|
||||
{
|
||||
throw new IllegalArgumentException("A render target is 203, 300 or 600 dpi.");
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Initial output contracts; new formats require a qualified renderer adapter.
|
||||
*******************************************************************************/
|
||||
@@ -93,7 +104,99 @@ public final class GenerationPorts
|
||||
SCANNER_BARCODE_PAYLOAD_UNSUPPORTED,
|
||||
SCANNER_BARCODE_CHECKSUM_REQUIRED,
|
||||
SCANNER_BARCODE_CHECKSUM_MISMATCH,
|
||||
UNSUPPORTED_BARCODE_OPTION
|
||||
UNSUPPORTED_BARCODE_OPTION,
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// version 2 scene refusals; no released renderer names these for a version 1 scene //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
SCENE_INVALID,
|
||||
STOCK_MISMATCH,
|
||||
TARGET_REQUIRES_SCENE_2,
|
||||
ELEMENT_OUTSIDE_PRINTABLE_AREA,
|
||||
TEXT_DOES_NOT_FIT,
|
||||
VALUE_MISSING,
|
||||
VALUE_TOO_LONG,
|
||||
FONT_UNKNOWN,
|
||||
FONT_MISSING_GLYPH,
|
||||
RESERVED_FONT_NAME,
|
||||
BARCODE_DOES_NOT_FIT,
|
||||
GS1_INVALID,
|
||||
ASSET_UNKNOWN,
|
||||
PREVIEW_TOO_LARGE,
|
||||
RENDERER_TOO_OLD
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** One renderer finding about a layout: a code from a closed vocabulary, the
|
||||
** element id the template author wrote, and numbers. It has no free text, so
|
||||
** no record value, context text or exception message can travel in it.
|
||||
***************************************************************************/
|
||||
public record Diagnostic(String code, Severity severity, String element, List<Integer> box, Map<String, Integer> measured, Map<String, Integer> limit)
|
||||
{
|
||||
private static final java.util.Set<String> WARNINGS = java.util.Set.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN",
|
||||
"BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED", "ELEMENTS_OVERLAP", "IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN");
|
||||
private static final java.util.Set<String> INFORMATION = java.util.Set.of("TEXT_SHRUNK", "TEXT_WRAPPED", "BARCODE_MODULE", "IMAGE_SCALED");
|
||||
|
||||
/************************************************************************
|
||||
** An error stops printing; a warning and information do not.
|
||||
************************************************************************/
|
||||
public enum Severity
|
||||
{
|
||||
ERROR, WARNING, INFO
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
** Admit only the vocabulary: an error is a FailureCode name, a warning or
|
||||
** information is one of the fixed names. The element is absent or an
|
||||
** author-written id, the box is absent or x, y, width and height in dots,
|
||||
** and measured values and limits are at most eight named integers each.
|
||||
************************************************************************/
|
||||
public Diagnostic
|
||||
{
|
||||
if(severity == null || code == null || !known(code, severity))
|
||||
{
|
||||
throw new IllegalArgumentException("Unknown diagnostic code for its severity.");
|
||||
}
|
||||
if(element != null && !element.matches("[A-Za-z][A-Za-z0-9_-]{0,31}"))
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid diagnostic element id.");
|
||||
}
|
||||
if(box != null && (box.size() != 4 || box.stream().anyMatch(java.util.Objects::isNull) || box.get(2) < 0 || box.get(3) < 0))
|
||||
{
|
||||
throw new IllegalArgumentException("A diagnostic box is x, y, width and height in dots.");
|
||||
}
|
||||
box = box == null ? null : List.copyOf(box);
|
||||
measured = numbers(measured);
|
||||
limit = numbers(limit);
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
** The closed vocabulary, by severity.
|
||||
************************************************************************/
|
||||
private static boolean known(String code, Severity severity)
|
||||
{
|
||||
return (switch(severity)
|
||||
{
|
||||
case ERROR -> java.util.Arrays.stream(FailureCode.values()).anyMatch(failure -> failure.name().equals(code));
|
||||
case WARNING -> WARNINGS.contains(code);
|
||||
case INFO -> INFORMATION.contains(code);
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
** Freeze named integers; a name is a short identifier, never a sentence.
|
||||
************************************************************************/
|
||||
private static Map<String, Integer> numbers(Map<String, Integer> values)
|
||||
{
|
||||
if(values == null)
|
||||
{
|
||||
return (Map.of());
|
||||
}
|
||||
if(values.size() > 8 || values.entrySet().stream().anyMatch(entry -> entry.getKey() == null || !entry.getKey().matches("[a-z][A-Za-z0-9]{0,31}") || entry.getValue() == null))
|
||||
{
|
||||
throw new IllegalArgumentException("Diagnostic values are at most eight named integers.");
|
||||
}
|
||||
return (Map.copyOf(values));
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
@@ -103,14 +206,29 @@ public final class GenerationPorts
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final FailureCode code;
|
||||
private final transient Diagnostic diagnostic;
|
||||
|
||||
/***************************************************************************
|
||||
** Construct a native process error whose public message is the safe code.
|
||||
***************************************************************************/
|
||||
public RenderException(FailureCode code)
|
||||
{
|
||||
this(code, null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A refusal that also says where and by how much: the diagnostic, when
|
||||
** present, is the error with this same code. The message stays the code.
|
||||
***************************************************************************/
|
||||
public RenderException(FailureCode code, Diagnostic diagnostic)
|
||||
{
|
||||
super(java.util.Objects.requireNonNull(code).name());
|
||||
if(diagnostic != null && (diagnostic.severity() != Diagnostic.Severity.ERROR || !code.name().equals(diagnostic.code())))
|
||||
{
|
||||
throw new IllegalArgumentException("A refusal's diagnostic must be the error with the same code.");
|
||||
}
|
||||
this.code = code;
|
||||
this.diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
@@ -120,6 +238,14 @@ public final class GenerationPorts
|
||||
{
|
||||
return (code);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** The detail the worker gave with the refusal, or null when it gave none.
|
||||
***************************************************************************/
|
||||
public Diagnostic diagnostic()
|
||||
{
|
||||
return (diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -223,7 +349,99 @@ public final class GenerationPorts
|
||||
** Bounded worker job with all revision inputs captured before dispatch.
|
||||
*******************************************************************************/
|
||||
public record RenderRequest(String requestId, String actorId, Source source, Format format, Revision revision,
|
||||
Map<String, Object> context, Integer maximumBytes) {}
|
||||
Map<String, Object> context, Integer maximumBytes, Integer targetDpi)
|
||||
{
|
||||
/***************************************************************************
|
||||
** The render target is absent (the stock's own resolution) or a resolution
|
||||
** the renderer supports.
|
||||
***************************************************************************/
|
||||
public RenderRequest
|
||||
{
|
||||
renderTarget(targetDpi);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Existing callers render at the stock's own resolution.
|
||||
***************************************************************************/
|
||||
public RenderRequest(String requestId, String actorId, Source source, Format format, Revision revision,
|
||||
Map<String, Object> context, Integer maximumBytes)
|
||||
{
|
||||
this(requestId, actorId, source, format, revision, context, maximumBytes, null);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A layout preview job: the print path stopped before retention. It has no
|
||||
** request id and no source record because nothing is written for it.
|
||||
***************************************************************************/
|
||||
public record PreviewRequest(String actorId, Format format, Revision revision, Map<String, Object> context, Integer targetDpi, Integer maximumBytes)
|
||||
{
|
||||
/***************************************************************************
|
||||
** Refuse a job that names no format or content, an unsupported render
|
||||
** target or no output bound.
|
||||
***************************************************************************/
|
||||
public PreviewRequest
|
||||
{
|
||||
if(format == null || revision == null || maximumBytes == null || maximumBytes < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("A preview needs a format, a revision and an output limit.");
|
||||
}
|
||||
renderTarget(targetDpi);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** One preview page: a PNG whose pixels are printer dots at the render target.
|
||||
***************************************************************************/
|
||||
public record PreviewPage(byte[] png, Integer widthDots, Integer heightDots)
|
||||
{
|
||||
private static final byte[] SIGNATURE = {(byte) 0x89, 'P', 'N', 'G', 13, 10, 26, 10};
|
||||
|
||||
/***************************************************************************
|
||||
** Only a PNG with a stated size is a page; the bytes cannot change later.
|
||||
***************************************************************************/
|
||||
public PreviewPage
|
||||
{
|
||||
if(png == null || png.length < SIGNATURE.length || !java.util.Arrays.equals(png, 0, SIGNATURE.length, SIGNATURE, 0, SIGNATURE.length)
|
||||
|| widthDots == null || widthDots < 1 || heightDots == null || heightDots < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("A preview page is a PNG with its size in dots.");
|
||||
}
|
||||
png = png.clone();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Never expose the stored byte array to a consumer.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public byte[] png()
|
||||
{
|
||||
return (png.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** What a preview shows: the pages, whether they are exactly what prints,
|
||||
** whether the layout can print at all, every diagnostic, and the size and
|
||||
** hash of the output the pages were decoded from.
|
||||
***************************************************************************/
|
||||
public record Preview(List<PreviewPage> pages, Boolean exact, Boolean printable, List<Diagnostic> diagnostics, Long outputBytes, String outputSha256)
|
||||
{
|
||||
/***************************************************************************
|
||||
** Freeze the lists and refuse a contradiction: a layout with an error
|
||||
** diagnostic is not printable.
|
||||
***************************************************************************/
|
||||
public Preview
|
||||
{
|
||||
pages = List.copyOf(pages);
|
||||
diagnostics = List.copyOf(diagnostics);
|
||||
if(exact == null || printable == null || outputBytes == null || outputBytes < 0 || outputSha256 == null || !outputSha256.matches("[a-f0-9]{64}")
|
||||
|| (printable && diagnostics.stream().anyMatch(diagnostic -> diagnostic.severity() == Diagnostic.Severity.ERROR)))
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid preview result.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Temporary bounded worker result, discarded after retained intake.
|
||||
@@ -393,6 +611,16 @@ public final class GenerationPorts
|
||||
throw new QException("Renderer barcode reference is unavailable.");
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
** Optional layout preview: the print path stopped before retention, with
|
||||
** the pages decoded from the validated output. A renderer that does not
|
||||
** provide it refuses, so an existing adapter is unchanged.
|
||||
***********************************************************************/
|
||||
default Preview preview(PreviewRequest request) throws QException
|
||||
{
|
||||
throw new QException("Renderer preview is unavailable.");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Authorize assets and validate user source outside the application JVM.
|
||||
*******************************************************************************/
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
/*******************************************************************************
|
||||
** Layout preview processes for a template: sample, stress and record data.
|
||||
** The producer already calls this registration point; it registers nothing yet.
|
||||
*******************************************************************************/
|
||||
final class GenerationPreviewProcesses
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Metadata namespace.
|
||||
*******************************************************************************/
|
||||
private GenerationPreviewProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once by the producer after the generation and authoring processes.
|
||||
*******************************************************************************/
|
||||
static void register(QInstance instance, DocumentGenerationQBitConfig config) throws QException
|
||||
{
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** The "Label preview" section of the template record screen. The template
|
||||
** table already offers itself here after its own sections; nothing is added yet.
|
||||
*******************************************************************************/
|
||||
final class GenerationPreviewWidget
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Metadata namespace.
|
||||
*******************************************************************************/
|
||||
private GenerationPreviewWidget()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once for the template table, after its standard sections.
|
||||
*******************************************************************************/
|
||||
static void section(QTableMetaData table)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,7 @@ public final class GenerationTables
|
||||
table.withSection(new QFieldSection("advanced", new QIcon("settings"), Tier.T3, List.of("id", "createDate", "scriptArtifact", "assetIds")).withLabel("Advanced references")
|
||||
.withCollapsible(CollapsibleMetaData.INITIALLY_CLOSED)
|
||||
.withHelpContent(help("These are exact stored identities for support and provenance. Change scripts and included assets through the named actions; do not edit integrity references.")));
|
||||
GenerationPreviewWidget.section(table);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
/*******************************************************************************
|
||||
** The authoring action that converts a version 1 scene to version 2.
|
||||
** The producer already calls this registration point; it registers nothing yet.
|
||||
*******************************************************************************/
|
||||
final class GenerationUpgradeProcesses
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Metadata namespace.
|
||||
*******************************************************************************/
|
||||
private GenerationUpgradeProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once by the producer after the generation and authoring processes.
|
||||
*******************************************************************************/
|
||||
static void register(QInstance instance, DocumentGenerationQBitConfig config) throws QException
|
||||
{
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.documentgeneration.GenerationPorts.Diagnostic;
|
||||
import com.kingsrook.qbits.documentgeneration.GenerationPorts.Diagnostic.Severity;
|
||||
import com.kingsrook.qbits.documentgeneration.GenerationPorts.FailureCode;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/*******************************************************************************
|
||||
** The preview, diagnostic and render target ports: what each value admits, what
|
||||
** the defaults refuse, and that every existing constructor and adapter still
|
||||
** compiles and means what it did.
|
||||
*******************************************************************************/
|
||||
class GenerationPortsContractTest
|
||||
{
|
||||
private static final GenerationPorts.Stock STOCK = new GenerationPorts.Stock(101600, 25400, 3000, 3000, 3000, 3000, 203);
|
||||
private static final GenerationPorts.Revision REVISION = new GenerationPorts.Revision(1, 2, "orders", STOCK, null, "{}", null, List.of());
|
||||
private static final GenerationPorts.Source SOURCE = new GenerationPorts.Source("orders", "1");
|
||||
private static final byte[] PNG = {(byte) 0x89, 'P', 'N', 'G', 13, 10, 26, 10, 0};
|
||||
private static final String SHA = "0".repeat(64);
|
||||
|
||||
/***************************************************************************
|
||||
** An adapter written before the preview port: it implements what it always
|
||||
** did, and this class compiling is the source compatibility proof.
|
||||
***************************************************************************/
|
||||
private static final class ExistingAdapter implements GenerationPorts.IsolatedRenderer
|
||||
{
|
||||
/************************************************************************
|
||||
** Accept every revision.
|
||||
************************************************************************/
|
||||
@Override
|
||||
public void validate(String actorId, GenerationPorts.Revision revision)
|
||||
{
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
** Return fixed bytes.
|
||||
************************************************************************/
|
||||
@Override
|
||||
public GenerationPorts.Content render(GenerationPorts.RenderRequest request)
|
||||
{
|
||||
return (new GenerationPorts.Content(new byte[] {1}));
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** The seven-argument request and the one-argument refusal are unchanged, and
|
||||
** an existing adapter refuses the optional operations it never had.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void existingConstructorsAndAdaptersMeanWhatTheyDid() throws Exception
|
||||
{
|
||||
var request = new GenerationPorts.RenderRequest("request-1", "actor-1", SOURCE, GenerationPorts.Format.ZPL, REVISION, Map.of("record", Map.of()), 1024);
|
||||
assertNull(request.targetDpi());
|
||||
assertEquals(request, new GenerationPorts.RenderRequest("request-1", "actor-1", SOURCE, GenerationPorts.Format.ZPL, REVISION, Map.of("record", Map.of()), 1024, null));
|
||||
var refusal = new GenerationPorts.RenderException(FailureCode.BARCODE_FIT_TOO_SMALL);
|
||||
assertEquals(FailureCode.BARCODE_FIT_TOO_SMALL, refusal.code());
|
||||
assertEquals("BARCODE_FIT_TOO_SMALL", refusal.getMessage());
|
||||
assertNull(refusal.diagnostic());
|
||||
assertThrows(NullPointerException.class, () -> new GenerationPorts.RenderException(null));
|
||||
var adapter = new ExistingAdapter();
|
||||
assertArrayEquals(new byte[] {1}, adapter.render(request).bytes());
|
||||
adapter.validate("actor-1", REVISION);
|
||||
assertEquals("Renderer preview is unavailable.", assertThrows(QException.class,
|
||||
() -> adapter.preview(new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, REVISION, Map.of(), null, 1024))).getMessage());
|
||||
assertEquals("Renderer barcode reference is unavailable.", assertThrows(QException.class, adapter::barcodeReference).getMessage());
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Stored error codes are read back by name, so the released names keep their
|
||||
** order and the version 2 refusals follow them.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void failureCodesKeepTheReleasedNamesThenAddTheSceneTwoRefusals()
|
||||
{
|
||||
List<String> names = Arrays.stream(FailureCode.values()).map(Enum::name).toList();
|
||||
assertEquals(List.of("BARCODE_FIT_TOO_SMALL", "BARCODE_FIT_UNSUPPORTED", "BARCODE_FIT_BOX_OUTSIDE_STOCK", "BARCODE_FIT_GEOMETRY_MISMATCH", "INVALID_BARCODE_FIT_PROFILE",
|
||||
"INVALID_BARCODE_FIT_REQUEST", "BARCODE_SYMBOLOGY_NOT_ALLOWED", "SYMBOLOGY_ALLOWLIST_REQUIRED", "SCANNER_BARCODE_PROFILE_UNSUPPORTED", "SCANNER_BARCODE_PAYLOAD_UNSUPPORTED",
|
||||
"SCANNER_BARCODE_CHECKSUM_REQUIRED", "SCANNER_BARCODE_CHECKSUM_MISMATCH", "UNSUPPORTED_BARCODE_OPTION"), names.subList(0, 13));
|
||||
assertEquals(List.of("SCENE_INVALID", "STOCK_MISMATCH", "TARGET_REQUIRES_SCENE_2", "ELEMENT_OUTSIDE_PRINTABLE_AREA", "TEXT_DOES_NOT_FIT", "VALUE_MISSING", "VALUE_TOO_LONG",
|
||||
"FONT_UNKNOWN", "FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD"), names.subList(13, names.size()));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A render target is absent or a resolution the renderer supports, for a
|
||||
** render and for a preview alike.
|
||||
***************************************************************************/
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, 72, 200, 204, 301, 1200, -203})
|
||||
void anUnsupportedRenderTargetIsRefused(int dpi)
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.RenderRequest("request-1", "actor-1", SOURCE, GenerationPorts.Format.ZPL, REVISION, Map.of(), 1024, dpi));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, REVISION, Map.of(), dpi, 1024));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Each supported resolution is carried unchanged.
|
||||
***************************************************************************/
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {203, 300, 600})
|
||||
void aSupportedRenderTargetIsCarried(int dpi)
|
||||
{
|
||||
assertEquals(dpi, new GenerationPorts.RenderRequest("request-1", "actor-1", SOURCE, GenerationPorts.Format.ZPL, REVISION, Map.of(), 1024, dpi).targetDpi());
|
||||
assertEquals(dpi, new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, REVISION, Map.of(), dpi, 1024).targetDpi());
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A preview job names its format, its content and an output bound.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aPreviewRequestNeedsAFormatARevisionAndALimit()
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewRequest("actor-1", null, REVISION, Map.of(), null, 1024));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, null, Map.of(), null, 1024));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, REVISION, Map.of(), null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.ZPL, REVISION, Map.of(), null, 0));
|
||||
assertEquals(REVISION, new GenerationPorts.PreviewRequest("actor-1", GenerationPorts.Format.PDF, REVISION, Map.of(), null, 1).revision());
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A page is a PNG with a size, and neither the caller nor a consumer can
|
||||
** change its bytes afterwards.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aPreviewPageIsAnImmutablePngWithItsSize()
|
||||
{
|
||||
byte[] supplied = PNG.clone();
|
||||
var page = new GenerationPorts.PreviewPage(supplied, 812, 203);
|
||||
supplied[8] = 9;
|
||||
page.png()[8] = 9;
|
||||
assertArrayEquals(PNG, page.png());
|
||||
assertEquals(812, page.widthDots());
|
||||
assertEquals(203, page.heightDots());
|
||||
byte[] jpeg = PNG.clone();
|
||||
jpeg[1] = 'J';
|
||||
for(byte[] bytes : new byte[][] {null, new byte[0], Arrays.copyOf(PNG, 7), jpeg})
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewPage(bytes, 812, 203));
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewPage(PNG, null, 203));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewPage(PNG, 0, 203));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewPage(PNG, 812, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.PreviewPage(PNG, 812, 0));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A preview freezes its lists, states its output exactly, and cannot call a
|
||||
** layout with an error printable.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aPreviewIsFrozenAndCannotContradictItsDiagnostics()
|
||||
{
|
||||
var page = new GenerationPorts.PreviewPage(PNG, 812, 203);
|
||||
var error = new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", null, null, null);
|
||||
var warning = new Diagnostic("PRINTER_FONT_NOT_SHOWN", Severity.WARNING, null, null, null, null);
|
||||
List<GenerationPorts.PreviewPage> pages = new ArrayList<>(List.of(page));
|
||||
List<Diagnostic> diagnostics = new ArrayList<>(List.of(warning));
|
||||
var preview = new GenerationPorts.Preview(pages, false, true, diagnostics, 1234L, SHA);
|
||||
pages.clear();
|
||||
diagnostics.clear();
|
||||
assertEquals(List.of(page), preview.pages());
|
||||
assertEquals(List.of(warning), preview.diagnostics());
|
||||
assertThrows(UnsupportedOperationException.class, () -> preview.pages().clear());
|
||||
assertThrows(UnsupportedOperationException.class, () -> preview.diagnostics().clear());
|
||||
assertEquals(false, new GenerationPorts.Preview(List.of(), true, false, List.of(error), 0L, SHA).printable());
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, List.of(warning, error), 1234L, SHA));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), null, true, List.of(), 1234L, SHA));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, null, List.of(), 1234L, SHA));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, List.of(), null, SHA));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, List.of(), -1L, SHA));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, List.of(), 1234L, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, List.of(), 1234L, SHA.toUpperCase().replace('0', 'A')));
|
||||
assertThrows(NullPointerException.class, () -> new GenerationPorts.Preview(null, true, true, List.of(), 1234L, SHA));
|
||||
assertThrows(NullPointerException.class, () -> new GenerationPorts.Preview(List.of(page), true, true, null, 1234L, SHA));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** The design's example diagnostic, and every code of the first release under
|
||||
** its own severity only.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aDiagnosticAdmitsOnlyItsClosedVocabulary()
|
||||
{
|
||||
var example = new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", List.of(24, 131, 764, 56),
|
||||
Map.of("widthDots", 902, "sizeCentipoints", 700, "lines", 2), Map.of("widthDots", 764, "lines", 2));
|
||||
assertEquals(List.of(24, 131, 764, 56), example.box());
|
||||
assertEquals(902, example.measured().get("widthDots"));
|
||||
assertEquals(2, example.limit().get("lines"));
|
||||
for(FailureCode code : FailureCode.values())
|
||||
{
|
||||
assertEquals(code.name(), new Diagnostic(code.name(), Severity.ERROR, null, null, null, null).code());
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code.name(), Severity.WARNING, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code.name(), Severity.INFO, null, null, null, null));
|
||||
}
|
||||
for(String code : List.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED", "ELEMENTS_OVERLAP",
|
||||
"IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN"))
|
||||
{
|
||||
assertEquals(Severity.WARNING, new Diagnostic(code, Severity.WARNING, null, null, null, null).severity());
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code, Severity.ERROR, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code, Severity.INFO, null, null, null, null));
|
||||
}
|
||||
for(String code : List.of("TEXT_SHRUNK", "TEXT_WRAPPED", "BARCODE_MODULE", "IMAGE_SCALED"))
|
||||
{
|
||||
assertEquals(Severity.INFO, new Diagnostic(code, Severity.INFO, null, null, null, null).severity());
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code, Severity.ERROR, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code, Severity.WARNING, null, null, null, null));
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("RENDER_FAILED", Severity.ERROR, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("Customer Jane Example could not be printed", Severity.ERROR, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(null, Severity.ERROR, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("TEXT_DOES_NOT_FIT", null, null, null, null, null));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Nothing but an author-written id and numbers fits beside the code: a
|
||||
** sentence, a record value or a message has no field that will hold it.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aDiagnosticCannotHoldFreeText()
|
||||
{
|
||||
assertEquals("order_id-2", new Diagnostic("VALUE_MISSING", Severity.ERROR, "order_id-2", null, null, null).element());
|
||||
for(String element : List.of("", "9caption", "two words", "Jane Example, 1 Test Street", "a".repeat(33), "caption\n", "/record/name"))
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("VALUE_MISSING", Severity.ERROR, element, null, null, null), element);
|
||||
}
|
||||
for(List<Integer> box : List.of(List.<Integer>of(), List.of(1, 2, 3), List.of(1, 2, 3, 4, 5), List.of(1, 2, -3, 4), List.of(1, 2, 3, -4), Arrays.asList(1, null, 3, 4)))
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("ELEMENT_OUTSIDE_PRINTABLE_AREA", Severity.ERROR, "caption", box, null, null), String.valueOf(box));
|
||||
}
|
||||
assertEquals(List.of(-12, -4, 0, 0), new Diagnostic("ELEMENT_OUTSIDE_PRINTABLE_AREA", Severity.ERROR, "caption", List.of(-12, -4, 0, 0), null, null).box());
|
||||
Map<String, Integer> nullValue = new HashMap<>();
|
||||
nullValue.put("widthDots", null);
|
||||
Map<String, Integer> nullName = new HashMap<>();
|
||||
nullName.put(null, 1);
|
||||
Map<String, Integer> tooMany = new HashMap<>();
|
||||
for(int index = 0; index < 9; index++)
|
||||
{
|
||||
tooMany.put("value" + index, index);
|
||||
}
|
||||
for(Map<String, Integer> values : List.of(Map.of("Width", 1), Map.of("width dots", 1), Map.of("the value Jane Example is too long", 1), Map.of("w".repeat(33), 1), nullValue, nullName, tooMany))
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", null, values, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", null, null, values));
|
||||
}
|
||||
List<Integer> box = new ArrayList<>(List.of(1, 2, 3, 4));
|
||||
Map<String, Integer> measured = new HashMap<>(Map.of("widthDots", 902));
|
||||
var diagnostic = new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", box, measured, null);
|
||||
box.set(0, 99);
|
||||
measured.put("widthDots", 1);
|
||||
assertEquals(List.of(1, 2, 3, 4), diagnostic.box());
|
||||
assertEquals(Map.of("widthDots", 902), diagnostic.measured());
|
||||
assertEquals(Map.of(), diagnostic.limit());
|
||||
assertThrows(UnsupportedOperationException.class, () -> diagnostic.box().set(0, 99));
|
||||
assertThrows(UnsupportedOperationException.class, () -> diagnostic.measured().put("widthDots", 1));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** A refusal carries only the error diagnostic with its own code, and its
|
||||
** public message is still the code alone.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aRefusalCarriesOnlyItsOwnErrorDiagnostic()
|
||||
{
|
||||
var detail = new Diagnostic("TEXT_DOES_NOT_FIT", Severity.ERROR, "caption", List.of(24, 131, 764, 56), Map.of("widthDots", 902), Map.of("widthDots", 764));
|
||||
var refusal = new GenerationPorts.RenderException(FailureCode.TEXT_DOES_NOT_FIT, detail);
|
||||
assertEquals(FailureCode.TEXT_DOES_NOT_FIT, refusal.code());
|
||||
assertEquals(detail, refusal.diagnostic());
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refusal.getMessage());
|
||||
assertNull(new GenerationPorts.RenderException(FailureCode.TEXT_DOES_NOT_FIT, null).diagnostic());
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.RenderException(FailureCode.VALUE_MISSING, detail));
|
||||
assertThrows(IllegalArgumentException.class, () -> new GenerationPorts.RenderException(FailureCode.TEXT_DOES_NOT_FIT, new Diagnostic("TEXT_SHRUNK", Severity.INFO, "caption", null, null, null)));
|
||||
assertThrows(NullPointerException.class, () -> new GenerationPorts.RenderException(null, detail));
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.documentgeneration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/*******************************************************************************
|
||||
** The registration points for the preview, asset and upgrade packages exist and
|
||||
** are called, and add nothing: a host that registers the QBit gets exactly the
|
||||
** tables, processes, choices, widgets and template sections it got before.
|
||||
*******************************************************************************/
|
||||
class GenerationRegistrationHooksTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** The complete default wiring, as the released foundation registers it.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theProducerRegistersExactlyWhatItDidBeforeTheHooks() throws Exception
|
||||
{
|
||||
QInstance instance = instance();
|
||||
new DocumentGenerationQBitProducer().withQBitConfig(config()).produce(instance);
|
||||
assertEquals(List.of("document_generation_asset", "document_generation_batch", "document_generation_binding", "document_generation_control", "document_generation_event",
|
||||
"document_generation_item", "document_generation_queue", "document_generation_request", "document_generation_revision", "document_generation_stock",
|
||||
"document_generation_template", "document_generation_type", "orders"),
|
||||
List.copyOf(new TreeSet<>(instance.getTables().keySet())), "tables");
|
||||
assertEquals(List.of("document_generation_binding_author_create", "document_generation_queue_document_advance", "document_generation_queue_document_cancel",
|
||||
"document_generation_queue_document_reconcileItem", "document_generation_queue_document_recover", "document_generation_queue_document_status",
|
||||
"document_generation_request_document_download", "document_generation_request_document_reconcile", "document_generation_request_document_reprint",
|
||||
"document_generation_request_document_status", "document_generation_stock_author_create", "document_generation_stock_author_rename",
|
||||
"document_generation_template_author_asset", "document_generation_template_author_catalog", "document_generation_template_author_context",
|
||||
"document_generation_template_author_create", "document_generation_template_author_edit", "document_generation_template_author_naming",
|
||||
"document_generation_template_author_packingSlip", "document_generation_template_author_preview", "document_generation_template_author_publish",
|
||||
"document_generation_template_author_removeAsset", "document_generation_template_author_rename", "document_generation_template_author_replaceAsset",
|
||||
"document_generation_type_author_create", "document_generation_type_author_rename", "orders_document_enqueue", "orders_document_generate"),
|
||||
List.copyOf(new TreeSet<>(instance.getProcesses().keySet())), "processes");
|
||||
assertEquals(List.of("document_generation_asset", "document_generation_context_publications", "document_generation_formats", "document_generation_preview_source",
|
||||
"document_generation_preview_source_orders", "document_generation_retained_assets", "document_generation_revision",
|
||||
"document_generation_source_tables", "document_generation_stock", "document_generation_template", "document_generation_type"),
|
||||
List.copyOf(new TreeSet<>(instance.getPossibleValueSources().keySet())), "possible value sources");
|
||||
assertEquals(List.of(),
|
||||
List.copyOf(new TreeSet<>(instance.getWidgets().keySet())), "widgets");
|
||||
assertEquals(List.of("overview", "layout", "context", "advanced"),
|
||||
instance.getTable(GenerationTables.TEMPLATE).getSections().stream().map(QFieldSection::getName).toList(), "template sections");
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Each registration point, called directly, changes nothing.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void eachRegistrationPointAddsNothingYet() throws Exception
|
||||
{
|
||||
DocumentGenerationQBitConfig config = config();
|
||||
assertEquals(List.of(), GenerationAssetTables.tables(config));
|
||||
QInstance instance = instance();
|
||||
GenerationPreviewProcesses.register(instance, config);
|
||||
GenerationAssetProcesses.register(instance, config);
|
||||
GenerationUpgradeProcesses.register(instance, config);
|
||||
assertEquals(List.of(), List.copyOf(instance.getProcesses().keySet()));
|
||||
assertEquals(List.of("orders"), List.copyOf(instance.getTables().keySet()));
|
||||
QTableMetaData table = new QTableMetaData().withName("template");
|
||||
GenerationPreviewWidget.section(table);
|
||||
assertEquals(new QTableMetaData().withName("template").getSections(), table.getSections());
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** The smallest host: one backend and one source table.
|
||||
***************************************************************************/
|
||||
private static QInstance instance()
|
||||
{
|
||||
QInstance instance = new QInstance();
|
||||
instance.addBackend(new QBackendMetaData().withName("memory").withBackendType(MemoryBackendModule.class));
|
||||
instance.addTable(new QTableMetaData().withName("orders").withBackendName("memory").withPrimaryKeyField("id").withField(new QFieldMetaData("id", QFieldType.INTEGER)));
|
||||
return (instance);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Default limits, with one source table so the source choices are registered.
|
||||
***************************************************************************/
|
||||
private static DocumentGenerationQBitConfig config()
|
||||
{
|
||||
return (new DocumentGenerationQBitConfig().withBackendName("memory").withSourceTables(List.of("orders")));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user