This file contains detailed implementation reference for the appget.dev/java code generation system. Read sections on demand using offset/limit — do not load fully into context.
See also: PIPELINE.md for the full pipeline architecture diagrams.
| Section | Line | Topic |
|---|---|---|
| Multi-Layer Pipeline | ~25 | Full ASCII pipeline diagram and layer descriptions |
| Build Dependency Resolution | ~80 | Step-by-step build order with outputs |
| Portability Anti-Patterns | ~145 | Code examples for avoiding Java-specific idioms |
| Schema vs. Policy | ~210 | Proto = schema, specs.yaml = policy separation |
| TypeRegistry Pattern | ~235 | Type mapping tables and usage |
| Language Utility Pattern | ~275 | JavaNaming, CodeGenUtils, JavaUtils with examples |
| Protocol Buffers Layer | ~320 | Why protobuf, protoc plugins by language |
| SQL Schema-First Design | ~365 | SQLSchemaParser algorithm, type mapping, domain org |
| Metadata Field Naming | ~430 | Snake_case canonical form, per-language transforms |
| Gherkin Business Rules | ~445 | Feature file DSL, step patterns, operator phrases |
| Specification System | ~510 | Specification.java dual-path, CompoundSpec, MetadataContext, Rule.java |
| Language-Agnostic Intermediates | ~560 | models.yaml and specs.yaml format and usage |
| Code Generation Strategy | ~600 | All generators: ModelsToProto, SpecGen, OpenAPI, AppServer, HTML |
| ServerEmitter Abstraction | ~640 | Framework-agnostic emitter interface |
| Generated Server Components | ~665 | Per-model/view files, infrastructure files, features |
| Generated Server Error Handling | ~710 | GlobalExceptionHandler, 5 handlers |
| RuleEngine Details | ~740 | specs.yaml loading, buildSpec(), condition shapes |
| RuleService & MetadataExtractor | ~785 | Generated patterns, blocking rules list |
| Generated Server Classpath | ~835 | Classpath restriction on codegen imports |
| HtmlCrudGenerator | ~845 | Static HTML CRUD page generation |
| HTTP Test Harness | ~860 | YAML-driven test runner |
| Logging Configuration | ~875 | Log4j2 setup, levels, classes, format |
| Testing Strategy | ~935 | 16 test suites, organization, fixtures |
| Build Artifacts | ~1095 | Generated code layout, compiled output |
| Performance Benchmarks | ~1120 | Timing for each make target |
┌─────────────────────────────────────────────────┐
│ Layer 0: Gherkin Business Rules │
│ source: features/*.feature + metadata.yaml │
│ status: Source of truth for rules, committed │
└──────────┬──────────────────────────────────────┘
│ FeatureToSpecsConverter (Gherkin parser)
│ .feature files + metadata → specs.yaml
↓
┌─────────────────────────────────────────────────┐
│ Layer 1: Schema + Rules │
│ source: schema.sql + views.sql (SQL DDL) │
│ generated: specs.yaml (git-ignored) │
│ status: Source of truth for models │
└──────────┬──────────────────────────────────────┘
│ SQLSchemaParser (regex-based)
│ Multi-dialect SQL + view parsing
↓
┌─────────────────────────────────────────────────┐
│ Layer 2: Intermediate Representation │
│ output: models.yaml (models + views per domain) │
│ format: YAML with type mappings │
│ purpose: Intermediate representation │
└──────────┬──────────────────────────────────────┘
│ ModelsToProtoConverter + protoc
│ generates .proto files → protobuf Java classes
↓
┌─────────────────────────────────────────────────┐
│ Layer 3: Implementation (Protobuf) │
│ output: Java model + view classes (protobuf) │
│ location: build/generated/ (protoc output) │
│ features: MessageOrBuilder, Builder pattern │
│ subpackages: model/ and view/ per domain │
└──────────┬──────────────────────────────────────┘
│ SpecificationGenerator
│ generates specs + metadata POJOs
↓
┌─────────────────────────────────────────────────┐
│ Layer 4: Specifications + Metadata │
│ output: Specification classes + context POJOs │
│ features: Simple, compound, metadata-aware │
│ targets: Any protobuf model or view class │
└──────────┬──────────────────────────────────────┘
│ DescriptorRegistry
│ dynamic model discovery
↓
┌─────────────────────────────────────────────────┐
│ Layer 5: Runtime (Descriptor-Based) │
│ RuleEngine: specs.yaml-driven evaluation │
│ Specification: protobuf getField() API │
│ DefaultDataBuilder: DynamicMessage samples │
│ No hard-coded imports or if/else dispatch │
└─────────────────────────────────────────────────┘
Problem: Circular dependencies (generators need compilation, generated code needs models, specs reference models)
Solution: Isolated generator compilation
Step 1: compileGenerators (independent)
├─ Compiles: FeatureToSpecsConverter, SQLSchemaParser, ModelsToProtoConverter,
│ SpecificationGenerator, ProtoOpenAPIGenerator, AppServerGenerator
└─ Output: build/generators/*.class
Step 1b: featuresToSpecs (depends on compileGenerators)
├─ Runs: FeatureToSpecsConverter (features/*.feature + metadata.yaml → specs.yaml)
└─ Output: specs.yaml (git-ignored, generated intermediate)
Step 2: parseSchema (depends on compileGenerators)
├─ Runs: SQLSchemaParser (schema.sql + views.sql → models.yaml)
└─ Output: models.yaml with models + views (git-ignored)
Step 3: generateProto (depends on parseSchema only)
├─ Runs: ModelsToProtoConverter + protoc (models.yaml → .proto → Java)
└─ Output: protobuf model + view classes per domain (build/generated/)
Note: Business rules are NOT embedded in .proto files; they travel in specs.yaml
Step 3b: generateOpenAPI (depends on generateProto)
├─ Runs: ProtoOpenAPIGenerator (.proto files → openapi.yaml)
└─ Output: openapi.yaml (git-ignored)
Step 3c: generateServer (depends on compileGenerators + generateProto + generateSpecs)
├─ Runs: AppServerGenerator (models.yaml + specs.yaml → REST API)
├─ Output: generated-server/dev/appget/server/ (complete Spring Boot project)
└─ Note: RuleService uses pre-compiled spec classes (no runtime YAML parsing)
Step 4: generateSpecs (depends on compileGenerators, featuresToSpecs, parseSchema)
├─ Runs: SpecificationGenerator (specs.yaml + models.yaml → Java)
└─ Output: specification/generated/ + specification/context/
Step 4b: generateDescriptorRegistry (depends on compileGenerators, parseSchema)
├─ Runs: DescriptorRegistryGenerator (models.yaml → DescriptorRegistry.java)
└─ Output: src/main/java-generated/dev/appget/util/DescriptorRegistry.java
Step 5: compileJava (depends on generateSpecs, generateDescriptorRegistry)
├─ Compiles: Main code + generated code
└─ Output: build/classes/main/*.class
Step 6: test (depends on compileJava)
├─ Runs: JUnit 5 tests (0 failures, 0 errors expected)
└─ Result: 280/280 passing
Step 7: build (depends on test)
├─ Packages: JAR, distributions
└─ Output: build/libs/appget.dev-java.jar
Critical Rule: generateProto depends on parseSchema only (not on featuresToSpecs). Business rules are not embedded in proto. This breaks the circular dependency.
Why this dependency order:
- featuresToSpecs and parseSchema are INDEPENDENT (can run in parallel)
- generateProto waits only on parseSchema (models.yaml) — it does NOT wait for specs.yaml
- specs.yaml is used by downstream generators (SpecificationGenerator, RuleEngine) but NOT by ModelsToProtoConverter
- This design enables future language implementations to load rules from the same specs.yaml without regenerating proto
Note: generateServer depends on generateSpecs (spec classes must exist first) but runs separately from default pipeline. Generates complete Spring Boot REST API server in generated-server/.
appget.dev/java is a code generation platform targeting Go, Python, and Ruby. Avoid these Java idioms to keep generated code portable:
| Anti-Pattern | Problem | Use Instead |
|---|---|---|
Switch expressions (case -> value) |
Java 14+ only, no equivalent in Go/Python/Ruby | if-else chains |
Pattern matching vars (instanceof Type var) |
Java 25+ only, requires explicit casting in other languages | Explicit casting: Type var = (Type) obj |
Static initialization blocks (static { map.put(...) }) |
Not portable; replaced by package-level initialization in Go/Python/Ruby | Static factory methods returning initialized maps |
| Method overloading | Not supported in Python/Ruby; confusing in Go/Rust | Builder pattern for multiple constructor signatures |
Singleton pattern (INSTANCE field) |
Mutable state risk, concurrency hazard, not idiomatic in Go/Python/Rust | Static utility class with static methods (private constructor, no instances) |
// ❌ Not portable: switch expression
return switch (operator) {
case "==" -> true;
case "!=" -> false;
default -> null;
};
// ✅ Portable: if-else
if (operator.equals("==")) {
return true;
} else if (operator.equals("!=")) {
return false;
}
return null;// ❌ Not portable: pattern matching variable
if (target instanceof MessageOrBuilder mob) {
return evaluate(mob);
}
// ✅ Portable: explicit casting
if (target instanceof MessageOrBuilder) {
MessageOrBuilder mob = (MessageOrBuilder) target;
return evaluate(mob);
}// ❌ Not portable: static block
private static final Map<String, String> TYPES = new HashMap<>();
static {
TYPES.put("VARCHAR", "String");
TYPES.put("INT", "int");
}
// ✅ Portable: static factory method
private static final Map<String, String> TYPES = createTypeMapping();
private static Map<String, String> createTypeMapping() {
Map<String, String> map = new HashMap<>();
map.put("VARCHAR", "String");
map.put("INT", "int");
return map;
}Critical Rule: Proto files define SCHEMA (models, fields, types). specs.yaml defines POLICY (rules, authorization, outcomes).
Problem Avoided: Embedding business rules in proto custom options mixes schema with policy, creating tight coupling and making rules hard to update without regenerating proto files.
Solution: Clean separation:
- proto files (generated from models.yaml): Field names, types, proto type mappings only
- specs.yaml (generated from features + metadata): Rules, metadata requirements, blocking flags, success/failure status
Impact on Generators:
- ModelsToProtoConverter: Converts models.yaml → proto (schema only, no rules)
- RuleEngine: Loads rules from specs.yaml, not from proto
- All downstream generators (SpecificationGenerator, AppServerGenerator): Read rules from specs.yaml
This separation ensures that:
- Proto files remain language-agnostic schema contracts for all future implementations
- Rules can be updated without regenerating proto
- Each language implementation reads the same specs.yaml
All type mappings are consolidated in a single per-language registry. This is the primary extension point for adding new language implementations.
Interface: src/main/java/dev/appget/codegen/TypeRegistry.java
Java implementation: src/main/java/dev/appget/codegen/JavaTypeRegistry.java
TypeRegistry.INSTANCE.neutralToProto("decimal") // → "appget.common.Decimal"
TypeRegistry.INSTANCE.neutralToJava("decimal") // → "BigDecimal"
TypeRegistry.INSTANCE.neutralToOpenApi("datetime") // → ["string", "date-time"]Neutral types stored in models.yaml (language-agnostic):
| models.yaml type | Java (neutralToJava) | Proto (neutralToProto) | OpenAPI |
|---|---|---|---|
string |
String |
string |
string |
int32 |
int / Integer |
int32 |
integer/int32 |
int64 |
long / Long |
int64 |
integer/int64 |
float64 |
double / Double |
double |
number/double |
bool |
boolean / Boolean |
bool |
boolean |
date |
LocalDate |
google.protobuf.Timestamp |
string/date |
datetime |
LocalDateTime |
google.protobuf.Timestamp |
string/date-time |
decimal |
BigDecimal |
appget.common.Decimal |
string/decimal |
For new language implementations (Go, Python, Ruby): Create GoTypeRegistry implements TypeRegistry, PythonTypeRegistry implements TypeRegistry, etc. Each language writes exactly one registry class; all generators read from it. Never add type mappings inside individual generators.
appget_common.proto: Generated automatically by ModelsToProtoConverter when any domain has decimal fields. Contains the shared Decimal message (bytes unscaled, int32 scale).
Appget separates language-agnostic utilities from language-specific naming convention utilities. This separation is a primary extension point for multi-language portability.
Language-agnostic (CodeGenUtils.java): String operations that apply to any language — capitalize(), escapeString(), smartSplit(), findMatchingParen(). Shared across all generators, never language-specific.
Runtime naming (dev.appget.naming): The NamingConvention interface defines the cross-language contract for field-name resolution at runtime. Each language implements this with a static utility class. Specification.java and generators both call JavaNaming.toFieldAccessor() for snake_case-to-camelCase conversion.
| Utility class | Language | snake_case input |
Casing output |
|---|---|---|---|
JavaNaming.java |
Java | role_level |
roleLevel (camelCase) |
GoNaming.go |
Go | role_level |
RoleLevel (PascalCase) |
python_naming.py |
Python | role_level |
role_level (identity) |
rust_naming.rs |
Rust | role_level |
role_level (identity) |
node_naming.js |
Node/JS | role_level |
roleLevel (camelCase) |
Codegen-only (JavaUtils.java): Methods used only at generation time that don't belong in the runtime naming contract: snakeToPascal(), snakeToHeaderCase(), JAVA_TO_PROTO_TYPE.
// Language-agnostic (CodeGenUtils) — shared by all
CodeGenUtils.capitalize("role_level") // → "Role_level"
CodeGenUtils.escapeString("he said \"hi\"") // → "he said \\\"hi\\\""
// Runtime naming (JavaNaming) — field accessor resolution
JavaNaming.toFieldAccessor("role_level") // → "roleLevel"
// Codegen-only (JavaUtils) — generation-time transforms
JavaUtils.snakeToPascal("role_level") // → "RoleLevel"
JavaUtils.snakeToHeaderCase("role_level") // → "Role-Level"Key rule: Generators read snake_case from intermediates (models.yaml, specs.yaml) and call the language-specific utility at codegen time. Never store language-specific casing in intermediate files.
Protobuf is the shared schema and code generation medium for ALL future appget.dev language implementations.
Why Protobuf (not JSON, not custom YAML)?
- Type Safety: Strongly typed schema with compile-time validation in all major languages
- Cross-Language Runtime: Descriptor/reflection API available in Java, Python, Go, Ruby, Rust, JavaScript/TypeScript, C#
- Backward Compatibility: Wire format evolution (adding fields, changing types) is safe and tested
- Code Generation:
protocplugins exist for all target languages; no need to build custom generators per language - Ecosystem: Wide adoption means library support, documentation, and battle-tested implementations
Protoc Plugins by Language (using models.yaml via ModelsToProtoConverter):
| Language | Plugin | Output | Usage |
|---|---|---|---|
| Java | built-in | .java classes, MessageOrBuilder | appget.dev/java reference implementation |
| Python | built-in | .py dataclasses, descriptor API | future appget.dev/python |
| Go | built-in | .pb.go structs with descriptor API | future appget.dev/go |
| Ruby | built-in | .pb.rb classes | future appget.dev/ruby |
| Rust | rust-protobuf | .rs structs | future appget.dev/rust |
| JavaScript/TypeScript | ts-proto or pbjs | .ts/.js classes | future appget.dev/node |
| C# | built-in | .cs classes | future appget.dev/csharp |
All implementations use the same .proto files (generated from models.yaml) as source of truth, then layer language-specific generators on top.
- Single source of truth: Database schema defines domain models
- Type safety: SQL type constraints reflected in Java type system
- Consistency: All generated models aligned with database
- Auditability: Schema changes tracked in version control
- Reproducibility: Same schema → same models, always
Location: src/main/java/dev/appget/codegen/SQLSchemaParser.java
Algorithm:
- Read schema.sql file as string
- Extract CREATE TABLE statements (handle nested parentheses)
- For each table: extract columns, parse types/constraints, map to domain
- Read views.sql file (if provided)
- Extract CREATE VIEW statements via regex
- Resolve view column types from source table columns via alias mapping
- Generate models.yaml with models + views per domain
- Write to file
Key Features:
- Parenthesis matching: Correctly handles
DECIMAL(15,2)nested types - Type mapping: Comprehensive SQL → Java type conversion
- Constraint parsing: Respects
NOT NULL, wraps primitives appropriately - Domain assignment: comment-based detection (
-- auth domainbefore tables assigns domain) - Name preservation: table and column names kept as snake_case (e.g.,
users,severity_level) - View alias resolution:
p.content→ resolvesp→posts→ lookup column type - Aggregate functions: COUNT → long, SUM → BigDecimal, AVG → double
Primitives with Nullability:
int + nullable=false → int
int + nullable=true → Integer
double + nullable=false → double
double + nullable=true → Double
String + nullable=any → String (always nullable in Java)
BigDecimal + nullable=any → BigDecimal (can hold null)
LocalDate + nullable=any → LocalDate (can hold null)
Table Mapping (comment-based in schema.sql — -- <domain> domain before each table group):
-- auth domain: users, sessions → dev.appget.auth.model
-- social domain: posts, comments, likes, follows, reposts → dev.appget.social.model
-- admin domain: moderation_flags → dev.appget.admin.model
View Mapping (comment-based in views.sql — same -- <domain> domain pattern):
-- social domain: user_profile_view, post_detail_view,
comment_detail_view, feed_post_view → dev.appget.social.view
Package Generation:
- Domain
auth→dev.appget.auth.model - Domain
social→dev.appget.social.model/dev.appget.social.view - Domain
admin→dev.appget.admin.model
metadata.yaml and specs.yaml use snake_case field names (role_level, session_id, is_admin). This is the language-agnostic canonical form, consistent with models.yaml. Each language's codegen applies its own casing: Java → JavaNaming.toFieldAccessor, Go → GoNaming.toFieldAccessor, Python/Rust/Ruby → identity. HTTP header derivation: snakeToHeaderCase("role_level") → Role-Level → X-Roles-Role-Level.
Business rules are defined in human-friendly Gherkin .feature files, one per domain. The FeatureToSpecsConverter parses these files with io.cucumber:gherkin:38.0.0 and combines them with metadata.yaml to generate specs.yaml.
Location: src/main/java/dev/appget/codegen/FeatureToSpecsConverter.java
See also: docs/GHERKIN_GUIDE.md for the complete DSL reference.
Feature-level tags: @domain:auth assigns domain to all scenarios
Scenario-level tags:
@target:users- target model/view name (snake_case plural, matches SQL table name)@rule:UserEmailValidation- explicit rule name@blocking- rule causes 422 rejection when unsatisfied@view- target is a view (not a model)
Step patterns:
| Purpose | Pattern | Example |
|---|---|---|
| Simple condition | When <field> <operator_phrase> <value> |
When severity_level is greater than 8 |
| String condition | When <field> <operator_phrase> "<value>" |
When username does not equal "" |
| Compound AND | When all conditions are met: + data table |
See features/auth.feature |
| Compound OR | When any condition is met: + data table |
|
| Metadata req | Given <category> context requires: + data table |
Given sso context requires: |
| Success outcome | Then status is "<value>" |
Then status is "APPROVED" |
| Failure outcome | But otherwise status is "<value>" |
But otherwise status is "REJECTED" |
Operator phrases (natural language → symbol):
| Phrase | Symbol |
|---|---|
| equals | == |
| does not equal | != |
| is greater than | > |
| is less than | < |
| is at least | >= |
| is at most | <= |
features/admin.feature- 7 rules (ModerationFlags model)features/auth.feature- 10 rules (Users + Sessions models)features/social.feature- 10 rules (Posts, Comments, Follows models + 4 views)metadata.yaml- context POJOs (sso, roles, user, tenant) committed separately
io.cucumber.messages.types.*wildcard import conflicts withjava.lang.Exception— always use specific imports- "Otherwise" is not a standard Gherkin keyword — use
But otherwiseinstead - Gherkin version 38.0.0 (not 38.0.1 which doesn't exist)
- StringBuilder-based YAML output (not SnakeYAML dump) for exact format control
Location: src/main/java/dev/appget/specification/Specification.java
The isSatisfiedBy(T target) method uses a dual-path approach:
- Protobuf messages (
MessageOrBuilder): Uses protobuf descriptor API (getField(fieldDescriptor)) — no reflection - Lombok POJOs (metadata contexts like
SsoContext,RolesContext): Falls back to reflection-based getter invocation
Handles:
- Number comparison: int, long, Integer, Long, Double via
doubleValue() - BigDecimal comparison:
compareTo()for precise decimal comparison - Boolean comparison: direct equality
- String comparison:
equals(),compareTo()for ordering operators - Null handling: equality/inequality checks for null values
Location: src/main/java/dev/appget/specification/CompoundSpecification.java
Combines multiple Specification instances with AND or OR logic:
AND: All specifications must be satisfied (allMatch)OR: At least one specification must be satisfied (anyMatch)
Location: src/main/java/dev/appget/specification/MetadataContext.java
Stores typed POJO instances per category (sso, roles, user). Rules with requires: blocks check metadata context before evaluating the main condition.
Location: src/main/java/dev/appget/model/Rule.java
- Accepts
SpecificationorCompoundSpecificationas spec evaluate(T target)andevaluate(T target, MetadataContext metadata)- Checks metadata requirements first, then evaluates main spec
- Supports any target type via generics
models.yaml and specs.yaml are the shared intermediate representations consumed by ALL language implementations (Java, Python, Go, Ruby, Rust, etc.).
- Source: SQLSchemaParser
- Format: YAML with per-domain model/view definitions
- Content: Field names (snake_case), types (Java types), domains
- Usage: ModelsToProtoConverter reads this to generate .proto files for ANY language
- Stability: Name, type, field number must be stable across language implementations
- Source: FeatureToSpecsConverter
- Format: YAML with per-domain rules, metadata definitions, conditions, outcomes
- Content: Rule names, target models, conditions (simple + compound), metadata requirements, blocking flags, status values
- Usage: RuleEngine loads these directly (no proto option parsing); all language implementations use same rules
No generator should directly parse schema.sql, views.sql, or .feature files — these are internal implementation details of the Java subproject. Future language implementations will:
- Import models.yaml and specs.yaml from the appget.dev/java project
- Run their own generators against these intermediates
- Generate language-specific code independently
- Load rules from specs.yaml at runtime (not from proto)
Input: models.yaml
Output: .proto files → protoc → Java protobuf model + view classes in build/generated/
Note: Rules are NOT embedded in proto. All generators that need rules read specs.yaml directly.
Input: specs.yaml + models.yaml
Output: Specification classes in specification/generated/ + metadata POJOs in specification/context/
specs.yaml Format:
metadata:
sso:
fields:
- name: authenticated
type: boolean
- name: session_id
type: String
rules:
- name: UserEmailValidation
target:
type: model
name: users
domain: auth
blocking: true # Causes 422 rejection if unsatisfied
conditions:
- field: email
operator: "!="
value: ""
then:
status: "VALID_EMAIL"
else:
status: "INVALID_EMAIL"Condition shapes:
- Simple list:
conditions: [{ field, operator, value }] - Compound object:
conditions: { operator: AND/OR, clauses: [...] }
Target resolution: Uses models.yaml to resolve domain:type:name → fully qualified import path
Input: .proto files (generated by ModelsToProtoConverter)
Output: openapi.yaml (OpenAPI 3.0.0 REST API specification with full CRUD, security)
View paths are generated automatically alongside model paths. Each view gets two GET-only paths: /views/{kebab-name} (list) and /views/{kebab-name}/{id} (by ID). No POST/PUT/DELETE for views.
URL transform: post_detail_view → strip _view → post_detail → kebab → post-detail → /views/post-detail
AppServerGenerator is a framework-agnostic orchestrator that delegates all code emission to a ServerEmitter interface. SpringBootEmitter is the sole implementation.
Location: src/main/java/dev/appget/codegen/AppServerGenerator.java (orchestrator, ~650 lines)
| File | Purpose | Lines |
|---|---|---|
ServerEmitter.java |
Interface — 22 methods (one per output file) | ~270 |
SpringBootEmitter.java |
All Spring Boot strings (annotations, imports, class structures) | ~1400 |
EntityContext.java |
Per-model/view data passed to emitter (name, PK info, resource path) | ~110 |
MetadataEmitContext.java |
Metadata categories + field definitions for emitter | ~45 |
RuleEmitContext.java |
Pre-filtered rules + blocking map + target map for emitter | ~100 |
Rule: To change generated server code, edit SpringBootEmitter.java — never AppServerGenerator.java (which only handles orchestration and file I/O). To add a new framework, implement ServerEmitter.
Per-Model: 4 files each (Interface + InMemory impl + Service + Controller) — full CRUD (POST, GET, PUT, DELETE), rule validation via RuleService.
Per-View: 4 files each (Interface + InMemory impl + Service + Controller) — GET-only (/views/{kebab-name} list, /views/{kebab-name}/{id} by ID). No RuleService injection. View import path uses .view. not .model.
Per-Model:
{Model}Controller.java- REST endpoints (POST, GET, PUT, DELETE){Model}Service.java- Business logic with rule validation{Model}Repository.java- In-memory storage (ConcurrentHashMap)
Infrastructure (11 files):
Application.java- Spring Boot entry point (@SpringBootApplication)config/MetadataExtractor.java- Extract auth headers → MetadataContextservice/RuleService.java- Load + evaluate rules from specs.yamldto/RuleAwareResponse.java- Response with rule evaluation resultsdto/RuleEvaluationResult.java- Rule outcomesdto/RuleOutcome.java- Individual rule resultdto/ErrorResponse.java- Error formatexception/GlobalExceptionHandler.java- @ControllerAdvice error handling (5 handlers)exception/RuleViolationException.java- Rule validation failureexception/ResourceNotFoundException.java- Not found errorsexception/MetadataParsingException.java- Invalid metadata header typeapplication.yaml- Spring Boot configuration
Key Features:
- Automatic CRUD endpoints for all models
- Rules validated on POST/PUT (before save)
- Only blocking rules cause 422 rejection; informational rules reported in response
- RuleService uses pre-compiled spec classes directly (no runtime YAML parsing)
- MetadataExtractor reads typed HTTP headers into context POJOs via builder pattern
- Constructor injection for all dependencies
- Proper HTTP status codes (201, 200, 204, 422, 404, 400)
- In-memory repository (no database dependency for MVP)
GlobalExceptionHandler has 5 handlers in priority order:
RuleViolationException→ 422 UNPROCESSABLE_ENTITY (blocking rule failure)ResourceNotFoundException→ 404 NOT_FOUNDMetadataParsingException→ 400 BAD_REQUEST (invalid metadata header type)HttpMessageNotReadableException→ 400 BAD_REQUEST (malformed JSON body)Exception(catch-all) → 500 INTERNAL_SERVER_ERROR
All responses use the ErrorResponse DTO with OffsetDateTime timestamps (RFC 3339).
Jackson gotcha: Use direct ObjectMapper construction with mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS). Jackson2ObjectMapperBuilder.featuresToDisable() is unreliable when combined with .modules() and ProtobufModule.
Location: src/main/java/dev/appget/RuleEngine.java
The RuleEngine loads business rules from specs.yaml using loadRulesFromSpecs(String specsFile):
// Parse specs.yaml into Rule objects with pre-built Specification/CompoundSpecification
List<Rule<?>> rules = ruleEngine.loadRulesFromSpecs("specs.yaml");
rules.forEach(rule -> evaluate(target, metadata));Why from specs.yaml, not from proto?
- specs.yaml is the source of truth for rules (generated from features + metadata)
- Proto contains only schema (model definitions, field types)
- Separating schema from policy enables rules to be updated without regenerating proto files
- This pattern is portable across all future language implementations
Internal Pattern:
The buildSpec() method converts YAML condition objects into Specification or CompoundSpecification instances:
// Simple condition: { field: age, operator: ">", value: 18 }
// → Specification("age", ">", 18)
// Compound condition: { operator: "AND", clauses: [...] }
// → CompoundSpecification(Logic.AND, [Specification(...), ...])Pattern for Future Implementations: All language implementations should:
- Use protoc to generate models from shared .proto files
- Load rules directly from specs.yaml (not from proto custom options)
- Implement a RuleEngine equivalent that parses specs.yaml and evaluates conditions against protobuf model instances
- The logic is language-agnostic: parse condition objects, build specification instances, evaluate against target
RuleService Generated Pattern:
// Direct instantiation (no runtime YAML loading)
private final UserEmailValidation userEmailValidation = new UserEmailValidation();
private final AdminAuthorizationRequired adminAuthorizationRequired = new AdminAuthorizationRequired();
// instanceof grouping per target model
if (target instanceof Users) {
// blocking rules set hasFailures = true when unsatisfied
// informational rules just report outcome
// metadata-aware rules use evaluate(target, metadata)
}MetadataExtractor Generated Pattern:
// Header convention: X-{Category}-{SnakeToHeaderCase(field_name)}
String ssoAuthenticated = request.getHeader("X-Sso-Authenticated");
// Type-aware parsing: boolean → Boolean.parseBoolean, int → Integer.parseInt
SsoContext ssoContext = SsoContext.builder()
.authenticated(Boolean.parseBoolean(ssoAuthenticated))
.build();
context.with("sso", ssoContext);Bridge Architecture (RuleService → Specification classes):
specs.yaml defines rules → SpecificationGenerator creates spec classes
↓
AppServerGenerator reads specs.yaml
→ generates RuleService that instantiates spec classes directly
→ generates MetadataExtractor that reads X-{Category}-{Field} headers
→ no runtime YAML parsing, no @PostConstruct, no reflection
Blocking Rules (specs.yaml):
blocking: true→ unsatisfied rule causes 422 rejection (hasFailures = true)blocking: false(default) → informational, reported in outcomes but never blocks- Currently blocking: UserEmailValidation, UserSuspensionCheck, SessionActiveValidation, SessionTokenPresence, VerifiedUserRequirement, UsernamePresence, SeverityLevelValidation, ReasonPresence, AdminAuthorizationRequired, HighSeverityEscalation, ContentTargetValidation, PublicPostVerification, PostContentValidation, CommentCreationValidation, ActiveFollowValidation
The generated-server build.gradle excludes **/codegen/** from its source sets. Classes in dev.appget.specification (like Specification.java) CANNOT import from dev.appget.codegen. If utilities are needed at runtime, move them to a runtime-accessible package (e.g., dev.appget.naming) — never duplicate logic to work around the restriction.
Location: src/main/java/dev/appget/codegen/HtmlCrudGenerator.java
Input: models.yaml + specs.yaml (optional)
Output: 67 static HTML files in generated-html/ (git-ignored)
Standalone generator (NOT an emitter). Generates per-model pages (index/create/edit/view) and per-view pages (read-only index). Form actions match SpringBootEmitter REST routes (/{resource}). Static structural scaffold — list/detail pages do not display live data.
Spec: tests/http-tests.yaml — 28 endpoint tests (CRUD + views + error paths)
Runner: tests/run-http-tests.py — Python script, reads YAML, executes curl, colored output
Target: make test-http (requires server running on port 8080)
Agent: ~/.claude/agents/http-tester.md — generic agent for any project's HTTP tests
All non-generated Java classes include Log4j2 logging for debugging and operation tracing.
File: src/main/resources/log4j2.properties
Log Levels:
- DEBUG: Method entry/exit, detailed operation flow
- INFO: Important milestones — file loading, rule counts, evaluation results
- WARN: Non-critical issues (missing files, skipped operations)
- ERROR: Exceptions and error conditions with stack traces
All non-generated classes have logging:
FeatureToSpecsConverter.java- Gherkin .feature parsing, specs.yaml generationSQLSchemaParser.java- Schema parsing, table/view extraction, YAML generationModelsToProtoConverter.java- models.yaml + specs → .proto file generationSpecificationGenerator.java- Specification and metadata POJO generationProtoOpenAPIGenerator.java- Proto-first OpenAPI spec generationAppServerGenerator.java- Spring Boot REST API generationRuleEngine.java- Loading rules from specs.yaml and evaluating themSpecification.java- Specification evaluation with field resolutionCompoundSpecification.java- AND/OR compound condition evaluationMetadataContext.java- Metadata context managementDescriptorRegistry.java- Dynamic model discovery and registrationDefaultDataBuilder.java- Sample data generationRule.java- Rule evaluation with metadata requirements
[ISO8601_TIMESTAMP] [LEVEL] [THREAD] [CLASS] - message
[2026-02-10T18:48:07,277] DEBUG [main] RuleEngine - Entering main method
Edit src/main/resources/log4j2.properties:
# Development with detailed tracing
logger.dev_appget_codegen.level = DEBUG
# Production with minimal logging
logger.dev_appget_codegen.level = INFOPackage-specific loggers:
dev.appget.codegen- Code generators (DEBUG by default)dev.appget.specification- Specification evaluation (DEBUG by default)dev.appget.model- Rule evaluation (DEBUG by default)dev.appget- RuleEngine main (DEBUG by default)
Generator logger pattern:
private static final Logger logger = LogManager.getLogger(ClassName.class);Comprehensive unit tests in 16 suites covering all components (0 failures, 0 errors expected):
- Gherkin
.featurefile parsing - Simple condition extraction (6 operators)
- Value coercion (integer, boolean, string)
- YAML value formatting
- Feature file parsing (appget.feature → 6 rules, hr.feature → 1 rule)
- Rule structure verification (target, conditions, blocking, metadata)
- Full conversion with metadata + rules
- Structural equivalence with original specs.yaml
- Proto file generation from models.yaml
- Field type mapping (Java type → proto type)
- No rule options embedded in generated proto
- Service CRUD operations
- View proto generation
- Proto-first OpenAPI 3.0.0 specification generation
- Schema definitions for all models
- Full CRUD endpoint generation (GET, POST, PUT, DELETE)
- Type mapping validation (proto → OpenAPI)
- Security (Bearer auth) and operationId/tags
- YAML rule parsing and Java class generation
- Compound specification generation (AND/OR)
- Metadata context POJO generation (SsoContext, RolesContext, UserContext)
- View-targeting specification generation
- Proper package structure and imports
- RuleService has no TODO stubs
- MetadataExtractor has no TODO stubs
- RuleService imports and instantiates pre-compiled spec classes
- RuleService skips view-targeting rules
- RuleService groups by instanceof
- RuleService uses metadata-aware evaluate for auth rules
- RuleService has no @PostConstruct (no runtime YAML)
- Blocking logic: only blocking rules set hasFailures
- MetadataExtractor imports context POJOs
- MetadataExtractor reads correct X-{Category}-{Field} headers
- MetadataExtractor uses builder pattern with context.with()
- Service stub existence and CRUD method descriptors
- All 5 domain services verified
- RuleEngine loads rules from specs.yaml (not from proto custom options)
- Generic rule evaluation with any model/view
- Compound specification evaluation
- Metadata requirement validation
- Custom status values
- Multiple rule consistency
- View field evaluated against wrong model type returns failure (type mismatch guard)
- View field evaluated against correct view type succeeds
- AND logic (all conditions must be true)
- OR logic (at least one condition true)
- Single condition edge cases
- Category storage and retrieval
- Reflection-based POJO evaluation (metadata contexts are Lombok)
- Missing category handling
- Comparison operators: >, <, >=, <=, ==, !=
- Type handling: Number, BigDecimal, Boolean, String
- Edge cases and boundary values
- Invalid field/operator handling
- Registry contains all 7 models and views
- Lookup by name for each model/view
- Unknown model returns null
- Field descriptors accessible
- Build Employees/Salaries/View with generic defaults
- String fields get "Sample_" prefix
- Int fields default to 42, double to 42.0
src/test/java/dev/appget/
├── codegen/
│ ├── AppServerGeneratorTest.java
│ ├── CodeGenUtilsTest.java
│ ├── FeatureToSpecsConverterTest.java
│ ├── JavaTypeRegistryTest.java
│ ├── JavaUtilsTest.java
│ ├── ModelsToProtoConverterTest.java
│ ├── ProtoOpenAPIGeneratorTest.java
│ └── SpecificationGeneratorTest.java
├── conformance/
│ └── ConformanceTest.java
├── model/
│ └── RuleTest.java
├── service/
│ └── GrpcServiceTest.java
├── specification/
│ ├── CompoundSpecificationTest.java
│ ├── MetadataContextTest.java
│ └── SpecificationTest.java
└── util/
├── DescriptorRegistryTest.java
└── DefaultDataBuilderTest.java
@TempDir: Temporary directories for generated code output@BeforeEach: Setup with protobufnewBuilder()for fluent test object creation@DisplayName: Descriptive test names in test runner output
src/test/resources/conformance/inputs/metadata.yaml mirrors the project's metadata.yaml format.
When changing metadata.yaml structure (e.g., adding enabled field), update this fixture too or the conformance test fails.
Gotcha: Always use ./gradlew, never system gradle. The project requires Java 25; the system Gradle (snap) uses Java 21 and will fail to compile.
Run all tests:
make testRun specific test class:
./gradlew test --tests "dev.appget.specification.CompoundSpecificationTest"Run specific test method:
./gradlew test --tests "dev.appget.model.RuleTest.testRuleWithCompoundSpecification"Run with full pipeline (recommended):
make all
# Runs: clean → parse-schema → generate → test → buildAfter running tests, generated code in:
src/main/java-generated/
├── dev/appget/model/ (5 models)
├── dev/appget/view/ (1 view)
├── dev/appget/hr/model/ (2 models)
├── dev/appget/hr/view/ (1 view)
├── dev/appget/finance/model/ (1 model)
└── dev/appget/specification/
├── generated/ (6 specs)
└── context/ (3 metadata POJOs)
Compiled output in:
build/
├── classes/main/ (compiled .class files)
├── classes/test/ (test classes)
├── libs/appget.dev-java.jar (executable JAR)
└── reports/tests/test/ (HTML test report)
make clean: ~0.7smake parse-schema: ~0.9smake generate: ~1smake test: ~2s (0 failures expected)make build: ~1smake all: ~5-6s total
Last Updated: 2026-04-03 Moved from: java/CLAUDE.md (deep reference content extracted to reduce context load)