Skip to content

Latest commit

 

History

History
952 lines (731 loc) · 29.7 KB

File metadata and controls

952 lines (731 loc) · 29.7 KB

CLAUDE.md

This file provides comprehensive AI-specific guidance for Claude Code when working with the library-api codebase.

For human-friendly quick start, see README.md. This file contains detailed architecture, troubleshooting, and AI assistant context.


Quick Reference

Core concept: DMN files in src/main/resources/ automatically generate REST endpoints at POST /api/v1/{path-to-dmn}

Start dev server: bin/dev (serves at http://localhost:8083)

Critical constraint: Every DMN model must have a Decision Service named {ModelName}Service to be exposed via API.


AI Assistant Guidelines

When Helping with DMN Files

  1. Always validate naming conventions:

    • Model names must be unique across all DMN files
    • Decision Service must be named {ModelName}Service (e.g., "PersonMinAge" → "PersonMinAgeService")
    • File names should be kebab-case (e.g., person-min-age.dmn)
  2. Import hierarchy is critical:

    • BDT.dmn is the root (defines tSituation and shared types)
    • Checks import BDT.dmn and relevant base modules (Age.dmn, Enrollment.dmn)
    • Benefits import BDT.dmn, Benefits.dmn, and needed check DMNs
    • All imports must namespace-qualify decision references
    • Imported decision services cannot have the same name, even if in different models
  3. Recommend test-driven development:

    • Suggest writing Bruno tests before implementing DMN logic
    • Tests are in test/bdt/ and should mirror DMN file structure
    • Each test file should focus on one scenario
  4. Hot reload workflow:

    • DMN changes are picked up automatically by Quarkus dev mode
    • No manual compilation needed
    • Endpoint appears immediately in Swagger UI

When Working with Java Code

IMPORTANT: Most Java code in target/generated-sources/kogito/ is auto-generated by Kogito. DO NOT edit these files.

Custom Java code (safe to edit):

  • src/main/java/org/codeforphilly/bdt/api/ - REST endpoints and OpenAPI generation (~1,828 LOC)
  • src/main/java/org/codeforphilly/bdt/functions/ - Custom FEEL functions (~242 LOC)

Key classes to understand:

  1. ModelRegistry.java (~554 LOC) - Discovers DMN models at startup

    • @Startup singleton that runs at application start
    • Scans classpath for all .dmn files (works in dev and JAR)
    • Parses DMN XML to extract metadata
    • Validates model name uniqueness
    • Caches ModelInfo objects for fast lookup
    • Provides findByPath() for endpoint routing
  2. DynamicDMNResource.java (~150 LOC) - Main REST controller

    • Handles all requests via POST /api/v1/{path}
    • Routes requests to appropriate DMN models via ModelRegistry
    • Builds DMN context from request JSON
    • Evaluates decision services using KIE DMN engine
    • Returns merged input+output context as JSON
  3. DynamicDMNOpenAPIFilter.java (~717 LOC) - Generates OpenAPI documentation

    • Implements MicroProfile OpenAPI filter
    • Generates OpenAPI path operations for each DMN model
    • Creates request/response schemas from DMN types
    • Groups endpoints by category (Benefits, Age Checks, etc.)
  4. DMNSchemaResolver.java (~289 LOC) - Type schema resolver

    • Converts DMN type definitions to JSON schemas
    • Handles complex types, collections, nested contexts
    • Generates example values for OpenAPI docs
  5. LocationService.java (~84 LOC) - SQLite database lookup

    • CDI bean providing static lookup() method
    • Queries embedded SQLite database (data/locations.db)
    • Callable from FEEL expressions in DMN files
    • Example: LocationService.lookup("zipCode", {"city": "Philadelphia"})
  6. DecisionServiceInvoker.java (~158 LOC) - Dynamic invocation

    • Programmatic DMN decision service invocation
    • Used for dynamic invocation without static imports
    • Maintains registry of known models and namespaces
    • Not currently usable from FEEL (CDI limitation)

Code Generation Caveats

What Kogito Generates:

  • REST resource classes (e.g., BenefitsResource.java, AgeResource.java)
  • Application configuration classes
  • DMN runtime wrappers

What This Project Uses Instead:

  • Custom DynamicDMNResource for all REST endpoints
  • Configuration sets kogito.generate.rest.decisions=false
  • Generated code exists but is not used for REST APIs

Why: Custom endpoints provide:

  • Better URL patterns (matching file structure)
  • Consistent response format (input+output merged)
  • Custom OpenAPI documentation
  • Path-based organization

Common Pitfalls to Avoid

  1. Duplicate model names: All DMN models must have globally unique names. Validated at startup.

  2. Incorrect service naming: Decision Service name must exactly match {ModelName}Service pattern or endpoint won't be generated.

  3. Missing namespace qualifiers: When calling imported decision services, always use namespace: ImportedModel.ServiceName(...)

  4. Editing generated code: Never suggest editing files in target/generated-sources/kogito/

  5. Assuming Kogito defaults: This project uses custom endpoints, not Kogito's default REST generation

  6. Breaking hot reload: Java changes require restart; only DMN changes hot reload

  7. Circular imports: DMN files cannot have circular import dependencies

  8. Type mismatches: Ensure imported types are properly qualified with namespace

Testing Guidance

Java tests (src/test/java/org/codeforphilly/bdt/api/):

  • Purpose: Validate internal behavior (model discovery, endpoint patterns, OpenAPI generation)
  • NOT for: Testing DMN business logic or specific eligibility rules
  • Test classes:
    • ModelDiscoveryTest.java - DMN model discovery, naming conventions, OpenAPI generation
    • DynamicEndpointPatternTest.java - Path mapping, URL patterns
    • SituationTypeValidationTest.java - tSituation type consistency
    • OpenAPISchemaPatternTest.java - OpenAPI schema generation
  • Run with: mvn test

Bruno API tests (test/bdt/):

  • Purpose: Validate API behavior and DMN decision logic
  • Structure:
    • collection.bru - Collection config (host: http://localhost:8083/api/v1)
    • environments/ - Test environments (local dev, prod)
    • benefits/ - Benefit endpoint tests
    • checks/ - Check endpoint tests
  • Organization: Test folders mirror DMN file paths
  • Run with: cd test/bdt && bru run

When adding new DMN models, always suggest creating corresponding Bruno tests.


DMN Architecture Patterns

Standard Check Pattern

Checks are reusable eligibility logic components.

Structure:

Decision Service: {CheckName}Service
├── Input: situation (tSituation)
├── Input: parameters (context with check-specific params)
└── Output: result (boolean)

File location: src/main/resources/checks/{category}/{check-name}.dmn

Endpoint: POST /api/v1/checks/{category}/{check-name}

Example: PersonMinAge check

  • Model: "PersonMinAge"
  • Service: "PersonMinAgeService"
  • File: checks/age/person-min-age.dmn
  • Endpoint: /api/v1/checks/age/person-min-age
  • Input: {situation, parameters: {minAge: number}}
  • Output: {result: boolean}

Implementation pattern:

  1. Extract primary person from situation.people
  2. Call "age as of date" BKM from BDT.dmn
  3. Compare age to parameters.minAge
  4. Return true/false

Standard Benefit Pattern

Benefits compose multiple checks into specific program eligibility rules.

Structure:

Decision Service: {BenefitName}Service
├── Input: situation (tSituation)
└── Outputs:
    ├── checks (context with individual check results)
    └── isEligible (boolean - typically all(checksAsList))

File location: src/main/resources/benefits/{jurisdiction}/{benefit-name}.dmn

Endpoint: POST /api/v1/benefits/{jurisdiction}/{benefit-name}

Response type: tBenefitResponse

{
  "situation": { /* echoed back */ },
  "checks": {
    "checkName1": boolean,
    "checkName2": boolean,
    "checkName3": boolean
  },
  "isEligible": boolean
}

Example: PhlHomesteadExemption

  • Checks:
    • age65Plus: Calls PersonMinAgeService with minAge=65
    • ownerOccupant: Reads situation.simpleChecks.ownerOccupant
    • livesInPhiladelphia: Reads situation.simpleChecks.livesInPhiladelphiaPa
    • notEnrolledInHomestead: Calls PersonNotEnrolledInBenefitService
  • Eligibility: all([age65Plus, ownerOccupant, livesInPhiladelphia, notEnrolledInHomestead])

Implementation pattern:

  1. Create decision for each eligibility criterion
  2. Aggregate checks into checks context decision
  3. Create isEligible decision using all([...]) on checks
  4. Include both in Decision Service outputs

The tSituation Data Model

Standard input type for all decisions. Represents household circumstances.

Core fields:

tSituation {
  primaryPersonId: string              // ID of person being evaluated
  people: tPersonList                  // Array of household members
    [{
      id: string
      dateOfBirth: date
    }]
  enrollments: tEnrollmentList         // Current benefit enrollments
    [{
      personId: string
      benefit: string
    }]
  relationships: tRelationshipList     // Household relationships
    [{
      type: string                     // e.g., "spouse", "child"
      personId: string
      relatedPersonId: string
    }]
  simpleChecks: tSimpleChecks          // Boolean flags for common checks
    {
      ownerOccupant: boolean
      livesInPhiladelphiaPa: boolean
      // ... other simple yes/no checks
    }
}

Extensible: New fields can be added to tSituation in BDT.dmn as needed.

Usage patterns:

  • Extract primary person: situation.people[item.id = situation.primaryPersonId]
  • Find specific person: situation.people[item.id = "someId"]
  • Check enrollment: count(situation.enrollments[item.benefit = "benefitName"]) > 0
  • Access simple checks: situation.simpleChecks.ownerOccupant

Detailed Workflow Recommendations

Adding a New Benefit (Step-by-Step)

  1. Plan the benefit:

    • Identify eligibility criteria
    • Determine which checks are reusable vs. inline
    • Decide on jurisdiction path (e.g., pa/phl/ for Philadelphia, PA)
  2. Create test cases first (TDD):

    • Create folder: test/bdt/benefits/{jurisdiction}/{BenefitName}/
    • Write tests:
      • Smoke Test.bru - Basic happy path
      • Eligible - {Scenario}.bru - Valid scenarios
      • Ineligible - {Reason}.bru - Each failing criterion
      • Edge Case - {Description}.bru - Boundary conditions
  3. Create DMN file:

    • Location: src/main/resources/benefits/{jurisdiction}/{benefit-name}.dmn
    • Use VS Code with DMN Editor extension
  4. Set up imports:

    • Import BDT.dmn (for tSituation and shared types)
    • Import Benefits.dmn (for tBenefitResponse)
    • Import needed check DMNs (e.g., person-min-age.dmn)
  5. Define Decision Service:

    • Name: {BenefitName}Service (CRITICAL: must follow pattern)
    • Input: situation (type: tSituation from BDT)
    • Outputs: checks (context), isEligible (boolean)
  6. Implement check decisions:

    • For reusable checks: CheckModel.CheckService(situation, {params}).result
    • For inline checks: FEEL expressions using situation fields
    • Name each decision descriptively (e.g., age65Plus, incomeLimit)
  7. Create checks context decision:

    {
      "checkName1": checkName1Decision,
      "checkName2": checkName2Decision,
      "checkName3": checkName3Decision
    }
    
  8. Create isEligible decision:

    all([checkName1, checkName2, checkName3])
    
  9. Test:

    • Save DMN file
    • Check Swagger UI for new endpoint
    • Run Bruno tests: cd test/bdt && bru run benefits/{jurisdiction}/{BenefitName}
    • Iterate on DMN logic based on test failures
  10. Document (optional):

    • Add description to DMN model metadata
    • Add comments in Bruno tests explaining scenarios

Creating a New Check (Step-by-Step)

  1. Determine category:

    • Existing: age, enrollment
    • New category: Create base module (see below)
  2. Create base module (if new category):

    • Location: src/main/resources/checks/{category}/{Category}.dmn
    • Define category-specific types (e.g., tIncomeSource, tIncomeList)
    • Import BDT.dmn
    • Provide BKMs for common operations if needed
  3. Create check DMN:

    • Location: src/main/resources/checks/{category}/{check-name}.dmn
    • Import BDT.dmn and relevant base module
  4. Define Decision Service:

    • Name: {CheckName}Service
    • Inputs:
      • situation (tSituation)
      • parameters (context with check-specific params)
    • Output: result (boolean)
  5. Implement logic:

    • Use FEEL expressions
    • Reference situation fields and parameters
    • Call BKMs from BDT.dmn or base modules as needed
  6. Create Bruno tests:

    • Folder: test/bdt/checks/{category}/{CheckName}/
    • Tests:
      • Pass.bru - Check succeeds
      • Fail.bru - Check fails
      • Edge cases as needed
  7. Test:

    • Save DMN file
    • Endpoint: /api/v1/checks/{category}/{check-name}
    • Run Bruno tests: cd test/bdt && bru run checks/{category}/{CheckName}
  8. Use in benefits:

    • Import check DMN in benefit file
    • Call: CheckModel.CheckService(situation, {param1: value1}).result

Modifying Existing DMN Files

Workflow:

  1. Open DMN in VS Code with DMN Editor extension
  2. Make changes (edit logic, add/remove decisions, modify types)
  3. Save (Ctrl+S or Cmd+S)
  4. Watch Quarkus console logs for "Reloading..."
  5. Test immediately in Swagger UI or rerun Bruno tests

Tips:

  • Keep Swagger UI open to see updated schemas
  • Use Bruno tests to verify changes didn't break existing behavior
  • For complex changes, access raw XML via "Reopen with Text Editor"
  • Test individual decisions in Swagger UI before composing in benefits

Common modifications:

  • Add a check to benefit: Add new decision → Update checks context → Update isEligible logic
  • Change check logic: Modify decision expression → Save → Test
  • Add parameter: Update Decision Service inputs → Update logic → Update Bruno tests
  • Refactor inline check to reusable: Create new check DMN → Import in benefit → Replace inline logic with call

Comprehensive Troubleshooting Guide

Endpoint Not Appearing

Symptoms: DMN file saved, but endpoint doesn't show in Swagger UI

Checklist:

  1. DMN file is in src/main/resources/ (or subdirectory)
  2. Decision Service is named {ModelName}Service exactly
  3. Model name is unique (no duplicate model names)
  4. Quarkus dev mode is running (bin/dev)
  5. Check startup logs for model discovery messages

Common causes:

  • Service named incorrectly (e.g., PersonMinAgeDecision instead of PersonMinAgeService)
  • Model name conflicts with existing model
  • DMN file has XML syntax errors
  • File not in resources directory

How to diagnose:

  1. Check Quarkus logs during startup for "Discovered X DMN models"
  2. Look for warnings about duplicate model names
  3. Verify DMN file opens in VS Code DMN Editor without errors
  4. Check ModelRegistry logs for discovery failures

Wrong Input/Output Types

Symptoms: Swagger UI shows unexpected schema, or API returns 400 errors

Checklist:

  1. Decision Service inputs/outputs are defined correctly
  2. Imported types use correct namespace prefix
  3. Type names match exactly (case-sensitive)
  4. tSituation structure matches expected format

Common causes:

  • Typo in type name (e.g., tSituations instead of tSituation)
  • Missing namespace prefix on imported types
  • Type defined in wrong DMN file
  • Field name mismatch in FEEL expressions

How to diagnose:

  1. Check Swagger UI for actual expected schema
  2. Compare with DMN Decision Service definition
  3. Verify imports are correctly configured
  4. Test with minimal request body first

Evaluation Errors

Symptoms: API returns 500 error, or results are incorrect

Checklist:

  1. FEEL expression syntax is valid
  2. Variable names match Decision names exactly
  3. Imported decisions are called with namespace: ModelName.ServiceName(...)
  4. All required parameters are provided

Common causes:

  • Typo in variable name (e.g., age65plus instead of age65Plus)
  • Missing namespace prefix on imported decision call
  • Incorrect parameter structure
  • Division by zero or null pointer in FEEL

How to diagnose:

  1. Check Quarkus dev logs (shows DMN evaluation details)
  2. Test individual decisions in Swagger UI first
  3. Simplify FEEL expression to isolate issue
  4. Use FEEL playground tools to test expressions

Debugging tips:

  • Add intermediate decisions to inspect values
  • Use simple FEEL expressions first, then make complex
  • Test imported decisions independently before composing

Import Not Working

Symptoms: Cannot reference decision from imported DMN file

Checklist:

  1. Imported DMN file is in correct location
  2. Import statement includes proper namespace URI
  3. Decision references use namespace prefix: ImportedModel.DecisionName
  4. No circular imports

Common causes:

  • Incorrect namespace URI in import statement
  • Missing namespace prefix in decision reference
  • Circular import (A imports B, B imports A)
  • Imported file has DMN errors

How to diagnose:

  1. Open imported DMN file in VS Code DMN Editor
  2. Verify namespace URI matches import statement
  3. Check for import warnings in Quarkus logs
  4. Test imported decision independently

Fix patterns:

  • Always use namespace prefix: PersonMinAge.PersonMinAgeService(...)
  • Copy namespace URI from imported DMN file's properties
  • Break circular imports by extracting shared types to third file

Hot Reload Not Working

Symptoms: DMN changes not picked up, old logic still runs

Checklist:

  1. File saved (check editor for unsaved indicator)
  2. Quarkus dev mode is running
  3. No compilation errors in console
  4. Browser cache cleared (for Swagger UI)

Common causes:

  • File not saved
  • Quarkus dev mode crashed (check console)
  • DMN has XML syntax errors preventing reload
  • Java changes mixed with DMN changes (requires restart)

How to fix:

  1. Stop and restart dev server: bin/dev
  2. Clean rebuild: mvn clean compile
  3. Check console for compilation errors
  4. Save file again and watch for reload message

Note: Only DMN changes hot reload. Java code changes require full restart.

Bruno Tests Failing

Symptoms: Bruno tests fail after DMN changes

Checklist:

  1. Dev server is running at http://localhost:8083
  2. Environment set to "local dev" in Bruno
  3. Request body JSON is valid
  4. Assertions match current response format

Common causes:

  • Dev server not running
  • Wrong environment selected
  • Response format changed (e.g., resultcheckResult)
  • Added new required field to tSituation

How to fix:

  1. Verify dev server: curl http://localhost:8083/q/health
  2. Check Bruno environment settings
  3. Update test assertions to match new response format
  4. Add new required fields to test request bodies

Tip: Run individual test first to isolate issue: cd test/bdt && bru run path/to/test.bru

Docker Build Fails

Symptoms: Docker build errors during deployment

Checklist:

  1. mvn clean package succeeds locally
  2. target/quarkus-app/ directory exists
  3. Docker daemon is running
  4. Dockerfile path is correct

Common causes:

  • Maven build failed (check pom.xml)
  • Missing dependencies
  • Wrong Dockerfile path
  • Insufficient disk space

How to fix:

  1. Run mvn clean package and fix any errors
  2. Verify target/quarkus-app/quarkus-run.jar exists
  3. Check Docker is running: docker ps
  4. Build locally first: docker build -f src/main/docker/Dockerfile.jvm -t test .

Model Discovery Errors

Symptoms: "Model not found" errors at runtime

Checklist:

  1. Path matches file location exactly
  2. File extension is .dmn
  3. ModelRegistry discovered the model (check startup logs)
  4. Model name matches path expectation

Common causes:

  • Path mismatch (e.g., /checks/age/person-min-age but file is person_min_age.dmn)
  • File in wrong directory
  • Model not discovered due to syntax errors
  • Casing issue (paths are case-sensitive)

How to diagnose:

  1. Check startup logs: "Discovered 8 DMN models: ..."
  2. Verify file path matches endpoint path
  3. Test in Swagger UI to see if endpoint exists
  4. Check ModelRegistry for the model

Important Constraints

These are strictly enforced and violations will cause runtime errors:

  1. Model Name Uniqueness:

    • All DMN models must have unique names (validated at startup)
    • Applies across all DMN files, even in different directories
    • Violation: Application fails to start with error message
  2. Service Naming:

    • Decision Services must follow {ModelName}Service pattern exactly
    • Case-sensitive, no spaces, no variations
    • Violation: Endpoint not generated, model not accessible via API
  3. No Duplicate Service Names:

    • Imported decision services cannot share names
    • Even if in different DMN models
    • Violation: Ambiguous references, evaluation errors
  4. Namespace Qualification:

    • All imported decision references must use namespace prefix
    • Format: ImportedModel.DecisionName
    • Violation: Decision not found, evaluation fails
  5. Java Version:

    • Must be Java 17 (not 21 like main BDT project)
    • Red Hat UBI 9 OpenJDK
    • Violation: Build failures, runtime errors
  6. Kogito Version:

    • Locked to 1.44.1.Final (compatibility with Quarkus 2.16.10)
    • Cannot upgrade without Quarkus upgrade
    • Violation: Dependency conflicts, build failures
  7. Hot Reload Scope:

    • Works for DMN changes only
    • Java changes require full restart
    • Violation: Changes not picked up, old code runs
  8. File Naming:

    • Use kebab-case for DMN files (e.g., person-min-age.dmn)
    • Lowercase, hyphens, no spaces
    • Violation: Inconsistency, harder to map to endpoints
  9. Decision Service Required:

    • Every exposed DMN model must have a Decision Service
    • Named {ModelName}Service
    • Violation: No endpoint generated
  10. Path Mapping:

    • Endpoint path must match file path exactly
    • /api/v1/{path-to-dmn-without-extension}
    • Violation: Model not found errors

Technology Stack Details

Quarkus 2.16.10.Final

  • Java application framework
  • Dev mode with hot reload (DMN only)
  • MicroProfile OpenAPI integration
  • Embedded Undertow server
  • CDI for dependency injection

Key dependencies:

  • quarkus-kogito-decisions - DMN engine integration
  • quarkus-smallrye-openapi - OpenAPI documentation
  • quarkus-jdbc-sqlite - Embedded database

Kogito 1.44.1.Final

  • DMN 1.3 specification implementation
  • KIE DMN engine for evaluation
  • FEEL expression language
  • Model compilation and caching

Limitations:

  • Cannot upgrade independently of Quarkus
  • Locked to specific KIE version
  • Some FEEL functions not available

Java 17 (Red Hat UBI 9 OpenJDK)

  • LTS version
  • Required for Quarkus 2.x compatibility
  • Different from main BDT project (uses Java 21)

Maven 3.8+

  • Build tool
  • Manages dependencies
  • Profiles for dev/prod builds

SQLite 3.x

  • Embedded database for location lookups
  • Located at src/main/resources/data/locations.db
  • Accessed via LocationService.lookup()

Bruno (Latest)

  • API testing tool
  • Collection-based organization
  • Environment management
  • CLI for CI/CD: bru run

Deployment Details

Semantic Versioning

Source of truth: pom.xml version field

Synchronized across:

  • Git tags: library-api-v{version} (e.g., library-api-v0.3.0)
  • Docker tags: :v{version} and :latest
  • Cloud Run revisions: library-api-v{version-with-dashes} (e.g., library-api-v0-3-0)

Version guidelines:

  • MAJOR (x.0.0): Breaking API changes
    • Removing endpoints
    • Changing response format
    • Renaming models
  • MINOR (0.x.0): New features (backwards compatible)
    • New benefits/checks
    • New endpoints
    • New fields in tSituation
  • PATCH (0.0.x): Bug fixes (backwards compatible)
    • Fix DMN logic bugs
    • Documentation updates
    • Dependency updates

Release Workflow

  1. Update version: ./bin/tag-release 0.4.0

    • Updates pom.xml using Maven
    • Commits change
    • Creates annotated git tag
  2. Review: git show

  3. Push:

    git push origin your-branch
    git push origin library-api-v0.4.0
  4. Automated deployment:

    • GitHub Actions triggered by tag push
    • Maven builds application
    • Docker image built and pushed
    • Cloud Run deployment with new revision

GitHub Actions Workflow

Triggered by: Tag push matching library-api-v*

Steps:

  1. Checkout code
  2. Set up Java 17
  3. Maven build: mvn clean package
  4. Validate version sync (tag vs pom.xml)
  5. Docker build using src/main/docker/Dockerfile.jvm
  6. Push to Google Artifact Registry with tags: :v{version}, :latest
  7. Deploy to Cloud Run with revision name

Google Cloud Run Configuration

Project: benefit-decision-toolkit-play Region: us-central1 Service: library-api Container Registry: us-central1-docker.pkg.dev/benefit-decision-toolkit-play/benefit-decision-toolkit-play/library-api

Settings:

  • Max instances: 2
  • Authentication: Allow unauthenticated (public API)
  • Port: 8080 (Cloud Run default)
  • CPU: 1
  • Memory: 512Mi

Service Account: library-api-service-account@benefit-decision-toolkit-play.iam.gserviceaccount.com


Advanced Topics

LocationService and SQLite Integration

Database: src/main/resources/data/locations.db

Schema:

  • Tables: zip_codes, cities, counties, states
  • Columns vary by table
  • Supports lookup by any column with filters

Usage from FEEL:

// Look up ZIP code by city
LocationService.lookup("zipCode", {"city": "Philadelphia", "state": "PA"})
// Returns: ["19102", "19103", "19104", ...]

// Look up county by ZIP
LocationService.lookup("county", {"zipCode": "19102"})
// Returns: "Philadelphia"

Implementation:

  • CDI-managed bean with @ApplicationScoped
  • Datasource injected via @Named("locations")
  • Constructs dynamic SQL queries from filter map
  • Returns list of strings (empty list if no matches)
  • Thread-safe for concurrent FEEL evaluations

Adding new lookups:

  1. Add data to SQLite database (use DB Browser for SQLite)
  2. No code changes needed (dynamic query construction)
  3. Use in DMN: LocationService.lookup("yourColumn", {"filter": "value"})

DecisionServiceInvoker

Purpose: Programmatic invocation of DMN decision services at runtime without static imports

Use case: Dynamic benefit evaluation where you don't know which benefits to check until runtime

Java usage (via CDI):

@Inject
DecisionServiceInvoker invoker;

public void evaluate() {
    Map<String, Object> situation = createSituation();
    Map<String, Object> params = Map.of("minAge", 65);

    Object result = invoker.invokeInternal(
        "benefits",                    // Model name
        "PhlHomesteadExemption",      // Decision service name
        situation,
        params
    );
}

Model registry:

known.put("benefits", "https://kie.apache.org/dmn/benefits-namespace");
known.put("age", "https://kie.apache.org/dmn/age-namespace");

Limitations:

  • Not currently callable from FEEL expressions (CDI container limitations)
  • Models must be manually registered in getKnownModels()
  • No auto-discovery of available models

Future enhancements:

  • Auto-discovery from classpath
  • Configuration-based registration (application.properties)
  • FEEL-accessible wrapper function

FEEL Expressions and Java Integration

Built-in FEEL functions:

// Date/time
date("2025-01-01")
today()
duration("P1Y")  // 1 year

// Lists
count([1, 2, 3])  // 3
sum([1, 2, 3])    // 6
all([true, true, false])  // false

// Strings
substring("hello", 1, 2)  // "he"
upper case("hello")       // "HELLO"

Custom Java functions from FEEL:

Requirements:

  1. Must be public static
  2. Must be in a class on the classpath
  3. Use full class path in FEEL

Example:

package org.codeforphilly.bdt.functions;

public class LocationService {
    public static List<String> lookup(String column, Map<String, Object> filters) {
        // implementation
    }
}

Called from FEEL:

org.codeforphilly.bdt.functions.LocationService.lookup("zipCode", {"city": "Philadelphia"})

Best practices:

  • Keep FEEL expressions simple and readable
  • Use Java functions for complex logic, database access, external APIs
  • Test Java functions independently before using in DMN
  • Document expected input/output types clearly

Custom OpenAPI Generation

How it works:

  1. At startup: Filter runs during OpenAPI schema generation
  2. Scans ModelRegistry: Gets all discovered DMN models
  3. Generates paths: Creates OpenAPI path for each model
  4. Resolves schemas: Converts DMN types to JSON schemas
  5. Groups endpoints: Categorizes by Benefits, Checks, etc.
  6. Adds examples: Generates sample request/response JSON

Customizing:

Edit DynamicDMNOpenAPIFilter.java:

if (modelInfo.getModelName().equals("PersonMinAge")) {
    operation.setDescription("Custom description for PersonMinAge");
}

Schema generation (DMNSchemaResolver):

  • Simple types: string, number, boolean, date → JSON primitives
  • Collections: tPersonList → array of tPerson objects
  • Contexts: Complex DMN types → JSON objects
  • Nested types: Resolves imported type references

When to Refer to README

Direct users to README.md for:

  • Quick start and getting started
  • High-level architecture overview
  • Common development tasks (adding benefits/checks)
  • Testing overview
  • Deployment quick reference
  • Key commands and configuration

This CLAUDE.md provides:

  • Detailed architecture and implementation patterns
  • Comprehensive troubleshooting guide
  • AI-specific guidance and common pitfalls
  • In-depth technical details
  • Java class descriptions and line counts
  • Advanced topics and future enhancements

Last Updated: 2025-01-21 | README Version: 0.3.0