Skip to content

Contextual Preferences

Contextual preferences in Bonsai allow you to override the normal tree traversal for specific keys. This is a powerful feature that enables user-specific customizations and overrides of default configurations.

Understanding Contextual Preferences

The Context class in Bonsai includes a preferences map, which maps keys to specific Knots. When evaluating a key, if the key is found in the preferences map, Bonsai will return the associated Knot directly, bypassing the normal tree traversal.

This allows you to provide user-specific overrides of default configurations without modifying the tree structure.

Creating a Context with Preferences

You can create a Context with preferences using the builder pattern:

// Create a custom Knot for a specific user
Knot customHomePageKnot = bonsai.createKnot(
    ValuedKnotData.builder().jsonValue("{\"theme\":\"dark\",\"layout\":\"compact\"}").build(),
    Map.of("description", "Custom home page configuration for user 123")
);

// Create a context with preferences
Map<String, Knot> preferences = Map.of("homePage", customHomePageKnot);
Context context = Context.builder()
    .documentContext(JsonPath.parse(userData))
    .preferences(preferences)
    .build();

// Evaluate the tree - will return the custom Knot directly
KeyNode result = bonsai.evaluate("homePage", context);

In this example, when evaluating the "homePage" key, Bonsai will return the customHomePageKnot directly, without traversing the tree.

Use Cases for Contextual Preferences

Contextual preferences are useful in several scenarios:

User-Specific Configurations

Allow users to customize their experience by overriding default configurations:

// Get user preferences from a database
Map<String, Knot> userPreferences = userPreferenceService.getPreferences(userId);

// Create a context with user preferences
Context context = Context.builder()
    .documentContext(JsonPath.parse(userData))
    .preferences(userPreferences)
    .build();

// Evaluate the tree with user preferences
KeyNode result = bonsai.evaluate("userInterface", context);

A/B Testing

Assign users to different test groups and override configurations accordingly:

// Determine which test group the user belongs to
String testGroup = abTestingService.getUserTestGroup(userId);

// Get the test configuration for the user's test group
Knot testConfigKnot = abTestingService.getTestConfiguration(testGroup);

// Create a context with test preferences
Map<String, Knot> preferences = Map.of("featureConfig", testConfigKnot);
Context context = Context.builder()
    .documentContext(JsonPath.parse(userData))
    .preferences(preferences)
    .build();

// Evaluate the tree with test preferences
KeyNode result = bonsai.evaluate("featureConfig", context);

Feature Flags

Override feature flags for specific users or environments:

// Determine which features are enabled for the user
Map<String, Knot> featureFlags = featureFlagService.getUserFeatureFlags(userId);

// Create a context with feature flags
Context context = Context.builder()
    .documentContext(JsonPath.parse(userData))
    .preferences(featureFlags)
    .build();

// Evaluate the tree with feature flags
KeyNode result = bonsai.evaluate("features.newUI", context);

Debugging and Testing

Override configurations for debugging or testing purposes:

// Create a debug configuration
Knot debugConfigKnot = bonsai.createKnot(
    ValuedKnotData.builder().jsonValue("{\"logLevel\":\"debug\",\"traceEnabled\":true}").build(),
    Map.of("description", "Debug configuration")
);

// Create a context with debug preferences
Map<String, Knot> preferences = Map.of("appConfig", debugConfigKnot);
Context context = Context.builder()
    .documentContext(JsonPath.parse(appData))
    .preferences(preferences)
    .build();

// Evaluate the tree with debug preferences
KeyNode result = bonsai.evaluate("appConfig", context);

Managing Preferences

In a real application, you would typically store preferences in a database and load them when creating the Context:

@Service
public class PreferenceService {
    private final Bonsai<Context> bonsai;
    private final PreferenceRepository preferenceRepository;

    @Autowired
    public PreferenceService(Bonsai<Context> bonsai, PreferenceRepository preferenceRepository) {
        this.bonsai = bonsai;
        this.preferenceRepository = preferenceRepository;
    }

    public Map<String, Knot> getUserPreferences(String userId) {
        // Load user preferences from the database
        List<Preference> preferences = preferenceRepository.findByUserId(userId);

        // Convert preferences to a map of keys to Knots
        Map<String, Knot> preferenceMap = new HashMap<>();
        for (Preference preference : preferences) {
            // Get or create the Knot for the preference
            Knot knot = getOrCreateKnot(preference);
            preferenceMap.put(preference.getKey(), knot);
        }

        return preferenceMap;
    }

    private Knot getOrCreateKnot(Preference preference) {
        // Check if the Knot already exists
        if (preference.getKnotId() != null) {
            return bonsai.getKnot(preference.getKnotId());
        }

        // Create a new Knot for the preference
        KnotData knotData = createKnotData(preference);
        Knot knot = bonsai.createKnot(knotData, Map.of("description", "User preference"));

        // Update the preference with the new Knot ID
        preference.setKnotId(knot.getId());
        preferenceRepository.save(preference);

        return knot;
    }

    private KnotData createKnotData(Preference preference) {
        // Create KnotData based on the preference type
        switch (preference.getType()) {
            case STRING:
                return ValuedKnotData.builder().stringValue(preference.getStringValue()).build();
            case BOOLEAN:
                return ValuedKnotData.builder().booleanValue(preference.getBooleanValue()).build();
            case NUMBER:
                return ValuedKnotData.builder().numberValue(preference.getNumberValue()).build();
            case JSON:
                return ValuedKnotData.builder().jsonValue(preference.getJsonValue()).build();
            default:
                throw new IllegalArgumentException("Unsupported preference type: " + preference.getType());
        }
    }
}

Custom Context with Preference Management

You can create a custom Context class that includes methods for managing preferences:

public class UserContext extends Context {
    private final String userId;
    private final PreferenceService preferenceService;

    public UserContext(String userId, Object data, PreferenceService preferenceService) {
        super(JsonPath.parse(data), preferenceService.getUserPreferences(userId));
        this.userId = userId;
        this.preferenceService = preferenceService;
    }

    public void addPreference(String key, Knot knot) {
        getPreferences().put(key, knot);
        preferenceService.savePreference(userId, key, knot);
    }

    public void removePreference(String key) {
        getPreferences().remove(key);
        preferenceService.deletePreference(userId, key);
    }

    public boolean hasPreference(String key) {
        return getPreferences().containsKey(key);
    }
}

Best Practices

  • Use preferences judiciously: Overriding the normal tree traversal should be done for specific use cases, not as a general approach
  • Document preference keys: Document the keys that can be overridden with preferences
  • Consider performance implications: Loading preferences from a database can impact performance
  • Implement caching: Cache preferences to improve performance
  • Validate preferences: Ensure that preferences are valid before using them
  • Consider security implications: Ensure that users can only override preferences they are allowed to
  • Test with and without preferences: Test your application both with and without preferences to ensure it works correctly in all scenarios