2026-10-02 13:13:27 +02:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
//
|
|
|
|
|
// Copyright (C) 2026- Equinor ASA
|
|
|
|
|
//
|
|
|
|
|
// ResInsight 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.
|
|
|
|
|
//
|
|
|
|
|
// ResInsight 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 at <http://www.gnu.org/licenses/gpl.html>
|
|
|
|
|
// for more details.
|
|
|
|
|
//
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
#include "RimWorkflowPortCompatibility.h"
|
|
|
|
|
|
|
|
|
|
#include "RimWorkflowDefinition.h"
|
|
|
|
|
#include "RimWorkflowSchemaTools.h"
|
|
|
|
|
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QMap>
|
|
|
|
|
#include <QSet>
|
|
|
|
|
#include <QStringList>
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
QString shortName( const QString& pythonType )
|
|
|
|
|
{
|
|
|
|
|
return pythonType.mid( pythonType.lastIndexOf( '.' ) + 1 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList pythonTypes( const QJsonObject& schema )
|
|
|
|
|
{
|
|
|
|
|
QStringList types;
|
|
|
|
|
for ( const char* key : { "x-taskmaestro-python-type", "x-ri-python-type" } )
|
|
|
|
|
{
|
|
|
|
|
const QString type = schema.value( key ).toString();
|
|
|
|
|
if ( !type.isEmpty() ) types.append( type );
|
|
|
|
|
}
|
|
|
|
|
for ( const QJsonValue& base : schema.value( "x-ri-python-bases" ).toArray() )
|
|
|
|
|
types.append( base.toString() );
|
|
|
|
|
return types;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonArray enumValues( const QJsonObject& schema )
|
|
|
|
|
{
|
|
|
|
|
if ( schema.contains( "const" ) ) return QJsonArray{ schema.value( "const" ) };
|
|
|
|
|
return schema.value( "enum" ).toArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isHiddenObjectValue( const QString& name, const QJsonObject& property, const QJsonObject& rootSchema )
|
|
|
|
|
{
|
|
|
|
|
// ObjectModel wraps its runtime object in an opaque `value` field that is not routable on its own
|
|
|
|
|
return name == "value" && RimWorkflowSchemaTools::isOpaque( property, rootSchema );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isModelCompatible( const QJsonObject& produced, const QJsonObject& expected )
|
|
|
|
|
{
|
|
|
|
|
const QString expectedType = expected.value( "x-ri-python-type" ).toString();
|
|
|
|
|
if ( !expectedType.isEmpty() && produced.contains( "x-ri-python-type" ) ) return pythonTypes( produced ).contains( expectedType );
|
|
|
|
|
|
|
|
|
|
if ( expected.contains( "title" ) && produced.contains( "title" ) && expected.contains( "properties" ) )
|
|
|
|
|
return expected.value( "title" ) == produced.value( "title" );
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isScalarCompatible( const QJsonObject& produced, const QJsonObject& expected )
|
|
|
|
|
{
|
|
|
|
|
const QString producedType = produced.value( "type" ).toString();
|
|
|
|
|
const QString expectedType = expected.value( "type" ).toString();
|
|
|
|
|
|
|
|
|
|
// bool is a subclass of int in Python, but int is not a subclass of float
|
|
|
|
|
if ( producedType != expectedType && !( producedType == "boolean" && expectedType == "integer" ) ) return false;
|
|
|
|
|
if ( produced.value( "format" ).toString() != expected.value( "format" ).toString() ) return false;
|
|
|
|
|
|
|
|
|
|
const QJsonArray expectedEnum = enumValues( expected );
|
|
|
|
|
if ( expectedEnum.isEmpty() ) return true;
|
|
|
|
|
const QJsonArray producedEnum = enumValues( produced );
|
|
|
|
|
if ( producedEnum.isEmpty() ) return false;
|
|
|
|
|
for ( const QJsonValue& value : producedEnum )
|
|
|
|
|
{
|
|
|
|
|
if ( !expectedEnum.contains( value ) ) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<RimWorkflowPort> findPort( const std::vector<RimWorkflowPort>& ports, const QString& name )
|
|
|
|
|
{
|
|
|
|
|
for ( const auto& port : ports )
|
|
|
|
|
{
|
|
|
|
|
if ( port.name == name ) return port;
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool hasRequiredConfigurableField( const QJsonObject& inputSchema, const QStringList& covered )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
|
|
|
|
for ( const QString& field : RimWorkflowSchemaTools::requiredFields( inputSchema ) )
|
|
|
|
|
{
|
|
|
|
|
if ( covered.contains( field ) ) continue;
|
|
|
|
|
if ( RimWorkflowSchemaTools::isConfigurable( properties.value( field ).toObject(), inputSchema ) ) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::expected<void, QString>
|
|
|
|
|
checkWholeToWhole( const QJsonObject& upstreamType, const QJsonObject& downstreamType, const QString& from, const QString& to )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject outputSchema = upstreamType.value( "output_schema" ).toObject();
|
|
|
|
|
const QJsonObject inputSchema = downstreamType.value( "input_schema" ).toObject();
|
|
|
|
|
if ( RimWorkflowPortCompatibility::isCompatible( outputSchema, outputSchema, inputSchema, inputSchema ) ) return {};
|
|
|
|
|
|
|
|
|
|
const QString mismatch = QString( "'%1' outputs %2, but '%3' expects %4" )
|
|
|
|
|
.arg( from, RimWorkflowPortCompatibility::typeName( outputSchema, outputSchema ) )
|
|
|
|
|
.arg( to, RimWorkflowPortCompatibility::typeName( inputSchema, inputSchema ) );
|
|
|
|
|
|
|
|
|
|
// taskmaestro merges the upstream fields with configured values when the task has config fields
|
|
|
|
|
const QStringList shared = RimWorkflowPortCompatibility::coveredByWholeOutput( upstreamType, downstreamType );
|
|
|
|
|
if ( shared.isEmpty() || !hasRequiredConfigurableField( inputSchema, shared ) ) return std::unexpected( mismatch );
|
|
|
|
|
|
|
|
|
|
const QJsonObject outputProperties = outputSchema.value( "properties" ).toObject();
|
|
|
|
|
const QJsonObject inputProperties = inputSchema.value( "properties" ).toObject();
|
|
|
|
|
for ( const QString& field : shared )
|
|
|
|
|
{
|
|
|
|
|
if ( !RimWorkflowPortCompatibility::isCompatible( outputProperties.value( field ).toObject(),
|
|
|
|
|
outputSchema,
|
|
|
|
|
inputProperties.value( field ).toObject(),
|
|
|
|
|
inputSchema ) )
|
|
|
|
|
{
|
|
|
|
|
return std::unexpected( QString( "Type mismatch for field '%1': %2" ).arg( field, mismatch ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RimWorkflowPort::isWhole() const
|
|
|
|
|
{
|
|
|
|
|
return name.isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// The whole input model first, then its fields
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::inputPorts( const QJsonObject& taskType )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject inputSchema = taskType.value( "input_schema" ).toObject();
|
|
|
|
|
|
|
|
|
|
std::vector<RimWorkflowPort> ports;
|
|
|
|
|
ports.push_back( { .name = "",
|
|
|
|
|
.schema = inputSchema,
|
|
|
|
|
.rootSchema = inputSchema,
|
|
|
|
|
.description = inputSchema.value( "description" ).toString(),
|
|
|
|
|
.typeName = typeName( inputSchema, inputSchema ),
|
|
|
|
|
.required = true } );
|
|
|
|
|
|
|
|
|
|
const QStringList required = RimWorkflowSchemaTools::requiredFields( inputSchema );
|
|
|
|
|
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
|
|
|
|
for ( const QString& name : RimWorkflowSchemaTools::propertyNames( inputSchema ) )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject property = properties.value( name ).toObject();
|
|
|
|
|
if ( isHiddenObjectValue( name, property, inputSchema ) ) continue;
|
|
|
|
|
ports.push_back( { .name = name,
|
|
|
|
|
.schema = property,
|
|
|
|
|
.rootSchema = inputSchema,
|
|
|
|
|
.description = property.value( "description" ).toString(),
|
|
|
|
|
.typeName = typeName( property, inputSchema ),
|
|
|
|
|
.required = required.contains( name ),
|
|
|
|
|
.configurable = RimWorkflowSchemaTools::isConfigurable( property, inputSchema ) } );
|
|
|
|
|
}
|
|
|
|
|
return ports;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// The whole output model first, then its fields
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::outputPorts( const QJsonObject& taskType )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject outputSchema = taskType.value( "output_schema" ).toObject();
|
|
|
|
|
|
|
|
|
|
std::vector<RimWorkflowPort> ports;
|
|
|
|
|
ports.push_back( { .name = "",
|
|
|
|
|
.schema = outputSchema,
|
|
|
|
|
.rootSchema = outputSchema,
|
|
|
|
|
.description = outputSchema.value( "description" ).toString(),
|
|
|
|
|
.typeName = typeName( outputSchema, outputSchema ) } );
|
|
|
|
|
|
|
|
|
|
const QJsonObject properties = outputSchema.value( "properties" ).toObject();
|
|
|
|
|
for ( const QString& name : RimWorkflowSchemaTools::propertyNames( outputSchema ) )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject property = properties.value( name ).toObject();
|
|
|
|
|
if ( isHiddenObjectValue( name, property, outputSchema ) ) continue;
|
|
|
|
|
ports.push_back( { .name = name,
|
|
|
|
|
.schema = property,
|
|
|
|
|
.rootSchema = outputSchema,
|
|
|
|
|
.description = property.value( "description" ).toString(),
|
|
|
|
|
.typeName = typeName( property, outputSchema ) } );
|
|
|
|
|
}
|
|
|
|
|
return ports;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::optional<RimWorkflowPort> RimWorkflowPortCompatibility::inputPort( const QJsonObject& taskType, const QString& name )
|
|
|
|
|
{
|
|
|
|
|
return findPort( inputPorts( taskType ), name );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::optional<RimWorkflowPort> RimWorkflowPortCompatibility::outputPort( const QJsonObject& taskType, const QString& name )
|
|
|
|
|
{
|
|
|
|
|
return findPort( outputPorts( taskType ), name );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// A schema without any type information accepts and produces anything
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RimWorkflowPortCompatibility::isAny( const QJsonObject& schema, const QJsonObject& rootSchema )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
|
|
|
|
for ( const char* key : { "type", "anyOf", "oneOf", "properties", "enum", "const", "x-taskmaestro-opaque", "$ref" } )
|
|
|
|
|
{
|
|
|
|
|
if ( resolved.contains( key ) ) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Whether every value of the produced schema is accepted by the expected schema. Models and
|
|
|
|
|
/// runtime objects compare their Python class and its bases, like `issubclass`.
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RimWorkflowPortCompatibility::isCompatible( const QJsonObject& produced,
|
|
|
|
|
const QJsonObject& producedRoot,
|
|
|
|
|
const QJsonObject& expected,
|
|
|
|
|
const QJsonObject& expectedRoot )
|
|
|
|
|
{
|
|
|
|
|
if ( isAny( produced, producedRoot ) || isAny( expected, expectedRoot ) ) return true;
|
|
|
|
|
|
|
|
|
|
const QJsonObject p = RimWorkflowSchemaTools::resolveReferences( produced, producedRoot );
|
|
|
|
|
const QJsonObject e = RimWorkflowSchemaTools::resolveReferences( expected, expectedRoot );
|
|
|
|
|
|
|
|
|
|
// Every produced option must be accepted; an expected union needs one accepting option
|
|
|
|
|
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
|
|
|
|
{
|
|
|
|
|
if ( !p.value( unionKey ).isArray() ) continue;
|
|
|
|
|
for ( const QJsonValue& option : p.value( unionKey ).toArray() )
|
|
|
|
|
{
|
|
|
|
|
if ( !isCompatible( option.toObject(), producedRoot, e, expectedRoot ) ) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
|
|
|
|
{
|
|
|
|
|
if ( !e.value( unionKey ).isArray() ) continue;
|
|
|
|
|
for ( const QJsonValue& option : e.value( unionKey ).toArray() )
|
|
|
|
|
{
|
|
|
|
|
if ( isCompatible( p, producedRoot, option.toObject(), expectedRoot ) ) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool producedOpaque = p.value( "x-taskmaestro-opaque" ).toBool( false );
|
|
|
|
|
if ( e.value( "x-taskmaestro-opaque" ).toBool( false ) )
|
|
|
|
|
{
|
|
|
|
|
return producedOpaque && pythonTypes( p ).contains( e.value( "x-taskmaestro-python-type" ).toString() );
|
|
|
|
|
}
|
|
|
|
|
if ( producedOpaque ) return false;
|
|
|
|
|
|
|
|
|
|
const QString producedType = p.value( "type" ).toString();
|
|
|
|
|
const QString expectedType = e.value( "type" ).toString();
|
|
|
|
|
if ( producedType == "object" || expectedType == "object" )
|
|
|
|
|
{
|
|
|
|
|
if ( producedType != expectedType ) return false;
|
|
|
|
|
if ( e.value( "additionalProperties" ).isObject() && !e.contains( "properties" ) )
|
|
|
|
|
{
|
|
|
|
|
return isCompatible( p.value( "additionalProperties" ).toObject(),
|
|
|
|
|
producedRoot,
|
|
|
|
|
e.value( "additionalProperties" ).toObject(),
|
|
|
|
|
expectedRoot );
|
|
|
|
|
}
|
|
|
|
|
return isModelCompatible( p, e );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( producedType == "array" || expectedType == "array" )
|
|
|
|
|
{
|
|
|
|
|
if ( producedType != expectedType ) return false;
|
|
|
|
|
return isCompatible( p.value( "items" ).toObject(), producedRoot, e.value( "items" ).toObject(), expectedRoot );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isScalarCompatible( p, e );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RimWorkflowPortCompatibility::isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected )
|
|
|
|
|
{
|
|
|
|
|
return isCompatible( produced.schema, produced.rootSchema, expected.schema, expected.rootSchema );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Short readable type name used in tooltips and error messages
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
QString RimWorkflowPortCompatibility::typeName( const QJsonObject& schema, const QJsonObject& rootSchema )
|
|
|
|
|
{
|
|
|
|
|
if ( isAny( schema, rootSchema ) ) return "Any";
|
|
|
|
|
|
|
|
|
|
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
|
|
|
|
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
|
|
|
|
{
|
|
|
|
|
if ( !resolved.value( unionKey ).isArray() ) continue;
|
|
|
|
|
QStringList names;
|
|
|
|
|
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
|
|
|
|
|
names.append( typeName( option.toObject(), rootSchema ) );
|
|
|
|
|
return names.join( " | " );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( resolved.value( "x-taskmaestro-opaque" ).toBool( false ) )
|
|
|
|
|
return shortName( resolved.value( "x-taskmaestro-python-type" ).toString( "object" ) );
|
|
|
|
|
if ( resolved.contains( "x-ri-python-type" ) ) return shortName( resolved.value( "x-ri-python-type" ).toString() );
|
|
|
|
|
|
|
|
|
|
const QString type = resolved.value( "type" ).toString();
|
|
|
|
|
if ( type == "object" ) return resolved.value( "title" ).toString( "object" );
|
|
|
|
|
if ( type == "array" ) return QString( "list[%1]" ).arg( typeName( resolved.value( "items" ).toObject(), rootSchema ) );
|
|
|
|
|
|
|
|
|
|
static const QMap<QString, QString> pythonNames = { { "string", "str" },
|
|
|
|
|
{ "integer", "int" },
|
|
|
|
|
{ "number", "float" },
|
|
|
|
|
{ "boolean", "bool" },
|
|
|
|
|
{ "null", "None" } };
|
|
|
|
|
const QString format = resolved.value( "format" ).toString();
|
|
|
|
|
if ( format == "date" ) return "date";
|
|
|
|
|
if ( format == "date-time" ) return "datetime";
|
|
|
|
|
if ( format == "path" || format == "file-path" || format == "directory-path" ) return "Path";
|
|
|
|
|
return pythonNames.value( type, type );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 14:38:42 +02:00
|
|
|
/// Port name -> type name, with an empty name for the whole model. Used to label ports in the graph.
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
QJsonObject RimWorkflowPortCompatibility::portTypes( const std::vector<RimWorkflowPort>& ports )
|
|
|
|
|
{
|
|
|
|
|
QJsonObject types;
|
|
|
|
|
for ( const auto& port : ports )
|
|
|
|
|
types[port.name] = port.typeName;
|
|
|
|
|
return types;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
/// Input fields of the downstream task supplied when it takes the whole upstream output
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
QStringList RimWorkflowPortCompatibility::coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject outputSchema = upstreamTaskType.value( "output_schema" ).toObject();
|
|
|
|
|
const QJsonObject inputSchema = downstreamTaskType.value( "input_schema" ).toObject();
|
|
|
|
|
const QStringList inputFields = RimWorkflowSchemaTools::propertyNames( inputSchema );
|
|
|
|
|
if ( isCompatible( outputSchema, outputSchema, inputSchema, inputSchema ) ) return inputFields;
|
|
|
|
|
|
|
|
|
|
const QStringList outputFields = RimWorkflowSchemaTools::propertyNames( outputSchema );
|
|
|
|
|
QStringList covered;
|
|
|
|
|
for ( const QString& field : inputFields )
|
|
|
|
|
{
|
|
|
|
|
if ( outputFields.contains( field ) ) covered.append( field );
|
|
|
|
|
}
|
|
|
|
|
return covered;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Whether adding the connection from -> to closes a cycle
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RimWorkflowPortCompatibility::wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to )
|
|
|
|
|
{
|
|
|
|
|
if ( from == to ) return true;
|
|
|
|
|
|
|
|
|
|
QStringList pending{ to };
|
|
|
|
|
QSet<QString> visited;
|
|
|
|
|
while ( !pending.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
const QString current = pending.takeLast();
|
|
|
|
|
if ( current == from ) return true;
|
|
|
|
|
if ( visited.contains( current ) ) continue;
|
|
|
|
|
visited.insert( current );
|
|
|
|
|
for ( const auto& edge : definition.edges )
|
|
|
|
|
{
|
|
|
|
|
if ( edge.from == current ) pending.append( edge.to );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Checks a new connection. An existing connection to the same input is replaced and ignored here.
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::expected<void, QString> RimWorkflowPortCompatibility::canConnect( const RimWorkflowDefinition& definition,
|
|
|
|
|
const QString& from,
|
|
|
|
|
const QString& output,
|
|
|
|
|
const QString& to,
|
|
|
|
|
const QString& input )
|
|
|
|
|
{
|
|
|
|
|
if ( from == to ) return std::unexpected( "A task cannot be connected to itself" );
|
|
|
|
|
if ( !definition.findNode( from ) ) return std::unexpected( QString( "Unknown task '%1'" ).arg( from ) );
|
|
|
|
|
if ( !definition.findNode( to ) ) return std::unexpected( QString( "Unknown task '%1'" ).arg( to ) );
|
|
|
|
|
|
|
|
|
|
const QJsonObject upstreamType = definition.taskTypeForNode( from );
|
|
|
|
|
const QJsonObject downstreamType = definition.taskTypeForNode( to );
|
|
|
|
|
if ( upstreamType.isEmpty() ) return std::unexpected( QString( "The task type of '%1' is not installed" ).arg( from ) );
|
|
|
|
|
if ( downstreamType.isEmpty() ) return std::unexpected( QString( "The task type of '%1' is not installed" ).arg( to ) );
|
|
|
|
|
|
|
|
|
|
const auto producedPort = outputPort( upstreamType, output );
|
|
|
|
|
if ( !producedPort ) return std::unexpected( QString( "'%1' has no output '%2'" ).arg( from, output ) );
|
|
|
|
|
const auto expectedPort = inputPort( downstreamType, input );
|
|
|
|
|
if ( !expectedPort ) return std::unexpected( QString( "'%1' has no input '%2'" ).arg( to, input ) );
|
|
|
|
|
|
|
|
|
|
// Edges into the same input are replaced by the new connection
|
|
|
|
|
RimWorkflowDefinition remaining = definition;
|
|
|
|
|
std::erase_if( remaining.edges, [&]( const RimWorkflowDefinitionEdge& edge ) { return edge.to == to && edge.input == input; } );
|
|
|
|
|
if ( wouldCreateCycle( remaining, from, to ) ) return std::unexpected( "The connection would create a cycle" );
|
|
|
|
|
|
|
|
|
|
for ( const auto& edge : remaining.incomingEdges( to ) )
|
|
|
|
|
{
|
|
|
|
|
if ( input.isEmpty() && !edge.isWholeInput() )
|
|
|
|
|
return std::unexpected( QString( "'%1' already has individual inputs connected and cannot also take a whole input" ).arg( to ) );
|
|
|
|
|
if ( !input.isEmpty() && edge.isWholeInput() )
|
|
|
|
|
return std::unexpected( QString( "'%1' takes its whole input from '%2'; disconnect it first" ).arg( to, edge.from ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( input.isEmpty() && output.isEmpty() ) return checkWholeToWhole( upstreamType, downstreamType, from, to );
|
|
|
|
|
|
|
|
|
|
if ( !isCompatible( *producedPort, *expectedPort ) )
|
|
|
|
|
{
|
|
|
|
|
const QString source = output.isEmpty() ? from : from + "." + output;
|
|
|
|
|
const QString target = input.isEmpty() ? to : to + "." + input;
|
|
|
|
|
return std::unexpected( QString( "%1 is %2, but %3 expects %4" ).arg( source, producedPort->typeName, target, expectedPort->typeName ) );
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|