Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@
import com.evolveum.polygon.conndev.spi.ValueMapping;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import org.identityconnectors.framework.common.objects.AttributeInfo;
import org.identityconnectors.framework.common.objects.AttributeInfoBuilder;
import org.identityconnectors.framework.common.objects.EmbeddedObject;
import org.identityconnectors.framework.common.objects.*;
import tools.jackson.databind.JsonNode;

import java.util.HashMap;
Expand Down Expand Up @@ -251,6 +249,22 @@ public ConnIdMapping connId() {
return connIdBuilder;
}

/**
* Special ConnId attributes (UID / NAME) are always exchanged as {@link String},
* regardless of the protocol's native type. Returns the type this attribute's
* protocol mapping must produce, or {@code null} when no forcing applies.
*/
Class<?> forcedConnIdType() {
// TOOD is this needed ?
if (connId().type().origin() == DefinitionValue.Origin.DECLARED) {
return null;
}
var connIdName = connId().name().value();
return (Uid.NAME.equals(connIdName) || Name.NAME.equals(connIdName))
? String.class
: null;
}

/**
* ConnId-side attribute metadata builder.
*
Expand Down Expand Up @@ -518,6 +532,7 @@ public Class<?> suggestedConnIdType() {
@Override
public AttributeProtocolMapping<?,?> build() {
ValueMapping<Object, JsonNode> implementation;
var forced = forcedConnIdType();

if (this.implementation != null) {
implementation = this.implementation;
Expand All @@ -530,7 +545,10 @@ public AttributeProtocolMapping<?,?> build() {
} else {
implementation = OpenApiValueMapping.from(type, openApiFormat);
}
if (connIdType != null && !connIdType.equals(implementation.connIdType())) {
if(forced!=null && !forced.equals(implementation.connIdType())){

implementation = ValueTypeOverrideMapping.of(forced, implementation);
} else if (connIdType != null && !connIdType.equals(implementation.connIdType())) {
// apply ConnId type override
implementation = ValueTypeOverrideMapping.of(connIdType, implementation);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.conndev.schema;

import com.evolveum.polygon.conndev.api.ContextLookup;
import com.evolveum.polygon.conndev.concepts.DefinitionValue;
import com.evolveum.polygon.conndev.concepts.SourceLocation;
import com.evolveum.polygon.conndev.json.JsonAttributeMapping;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.Uid;
import org.testng.annotations.Test;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;

import static org.testng.Assert.*;

/**
* Verifies that special ConnId attributes (UID / NAME) are always exposed as
* {@link String} on the ConnId side, even when the protocol (JSON) native type
* not textual — e.g. a schema whose native {@code id} attribute has an integer
* wire type while being declared as the UID attribute.
* <p>
* Covers both halves of the mechanism:
* <ul>
* <li>{@link AbstractAttributeBuilder#forcedConnIdType()} — which attributes are
* forced to String, and that only an explicitly <em>declared</em> ConnId type
* suppresses the forcing (framework defaults and rule-detected values do not),</li>
* <li>the {@link JsonAttributeMapping} produced by {@code json().type("integer")} —
* it reports {@code String} as its ConnId type and converts numeric wire values
* to/from String, while leaving the wire representation a JSON number.</li>
* </ul>
*/
public class ForcedConnIdTypeTest {

private static TestObjectClass newObjectClass() {
var schemaBuilder = new BaseSchemaBuilder(StubConnector.class, ContextLookup.none());
return new TestObjectClass(schemaBuilder, DefinitionValue.from("Test", SourceLocation.capture()));
}

private static TestAttributeBuilder newUidAttribute() {
var attribute = newObjectClass().attribute("id");
attribute.connId().name(Uid.NAME);
return attribute;
}

@Test
public void uidAttribute_forcesString() {
var attribute = newUidAttribute();
attribute.json().type("integer");

assertEquals(attribute.forcedConnIdType(), String.class);
}

@Test
public void nameAttribute_forcesString() {
var attribute = newObjectClass().attribute("login");
attribute.connId().name(Name.NAME);
attribute.json().type("integer");

assertEquals(attribute.forcedConnIdType(), String.class);
}

@Test
public void regularAttribute_isNotForced() {
var attribute = newObjectClass().attribute("count");
attribute.json().type("integer");

assertNull(attribute.forcedConnIdType());
}

@Test
public void defaultType_doesNotSuppressForcing() {
var attribute = newUidAttribute();
attribute.json().type("integer");

assertEquals(attribute.forcedConnIdType(), String.class);
}

@Test
public void detectedType_doesNotSuppressForcing() {
var attribute = newUidAttribute();
attribute.json().type("integer");
attribute.connId().type(DefinitionValue.detected( String.class));

assertEquals(attribute.forcedConnIdType(), String.class);
}

@Test
public void uidAttribute_integerWire_mappingReportsStringConnIdType() {
var attribute = newUidAttribute();
attribute.json().type("integer");

var mapping = attribute.build().json();

assertEquals(mapping.connIdType(), String.class);
}

@Test
public void uidAttribute_integerWire_deserializesNumberAsString() {
var attribute = newUidAttribute();
attribute.json().type("integer");

var mapping = attribute.build().json();
Object value = mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42));

assertTrue(value instanceof String, "UID value should be deserialized as String, got: " + value);
assertEquals(value, "42");
}

@Test
public void uidAttribute_integerWire_serializesStringBackToNumber() {
var attribute = newUidAttribute();
attribute.json().type("integer");

var mapping = attribute.build().json();
ObjectNode parent = JsonNodeFactory.instance.objectNode();

mapping.toJsonNode(AttributeBuilder.build(Uid.NAME, "42"), parent);

JsonNode node = parent.get("id");
assertNotNull(node);
assertTrue(node.isIntegralNumber(), "Wire value should stay a JSON number, got: " + node);
assertEquals(node.intValue(), 42);
}

@Test
public void nameAttribute_integerWire_forcedToString() {
var attribute = newObjectClass().attribute("login");
attribute.connId().name(Name.NAME);
attribute.json().type("integer");

var mapping = attribute.build().json();

assertEquals(mapping.connIdType(), String.class);
assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(7)), "7");
}

@Test
public void uidAttribute_int64Wire_forcedToString() {
var attribute = newUidAttribute();
attribute.json().type("integer").openApiFormat("int64");

var mapping = attribute.build().json();

assertEquals(mapping.connIdType(), String.class);
assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42)), "42");
}

@Test
public void uidAttribute_stringWire_noOverrideNeeded() {
var attribute = newUidAttribute();
attribute.json().type("string");

var mapping = attribute.build().json();

assertEquals(mapping.connIdType(), String.class);
assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.stringNode("abc")), "abc");
}

@Test
public void regularAttribute_integerWire_keepsNativeType() {
var attribute = newObjectClass().attribute("count");
attribute.json().type("integer");

var mapping = attribute.build().json();

assertEquals(mapping.connIdType(), Integer.class);
assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42)), 42);
}

@Test
public void uidAttribute_explicitType_mappingKeepsNativeType() {
var attribute = newUidAttribute();
attribute.json().type("integer");
attribute.connId().type(Integer.class);

var definition = attribute.build();

assertEquals(definition.connId().getType(), Integer.class);
assertEquals(definition.json().connIdType(), Integer.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,6 @@
*/
public class MappingRuleTest {

private static final class StubConnector implements Connector {
@Override
public Configuration getConfiguration() {
return null;
}

@Override
public void init(Configuration configuration) {
}

@Override
public void dispose() {
}
}

/** Minimal attribute builder: B, A and R collapsed into a single self-referential type. */
private static final class TestAttributeBuilder extends BaseAttributeBuilder<
TestAttributeBuilder, TestAttributeBuilder, TestAttributeBuilder, BaseAttributeDefinition> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.conndev.schema;

import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.Connector;

public class StubConnector implements Connector {
@Override
public Configuration getConfiguration() {
return null;
}

@Override
public void init(Configuration configuration) {
}

@Override
public void dispose() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.conndev.schema;

import com.evolveum.polygon.conndev.concepts.DefinitionValue;

/** Minimal attribute builder: B, A and R collapsed into a single self-referential type. */
public final class TestAttributeBuilder extends BaseAttributeBuilder<
TestAttributeBuilder, TestAttributeBuilder, TestAttributeBuilder, BaseAttributeDefinition> {
TestAttributeBuilder(BaseObjectClassDefinitionBuilder parent, DefinitionValue<String> name) {
super(parent, name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.conndev.schema;

import com.evolveum.polygon.conndev.concepts.DefinitionValue;

/** Minimal object class builder pairing with {@link TestAttributeBuilder}. */
public final class TestObjectClass extends BaseObjectClassDefinitionBuilder<
TestObjectClass,
BaseObjectClassDefinition<BaseAttributeDefinition>,
TestAttributeBuilder,
TestAttributeBuilder,
TestAttributeBuilder,
BaseAttributeDefinition> {

TestObjectClass(BaseSchemaBuilder parent, DefinitionValue<String> name) {
super(parent, name);
}

@Override
protected TestAttributeBuilder newAttribute(DefinitionValue<String> def) {
return new TestAttributeBuilder(this, def);
}
}