Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JEnv

a small Java library for loading .jenv configuration files because apparently Java needed another way to deal with configuration (yes, i made one) ... and i'm totally not competing with .env (if i were, i'd be lowkey DEAD)

About

JEnv is a small Java library for loading and accessing configuration files written in the JENV format!

i made this mainly to practice working with:

  • Java generics
  • Map
  • Path and file I/O
  • functional interfaces
  • sealed interfaces
  • records
  • custom exceptions
  • parsing
  • reusable library design
  • Maven
  • unit testing
  • designing a small public API
  • separating loading, configuration access, value handling, and error handling into their own components
  • actually making a library that another Java program can use instead of just making another program that reads input and prints something, mhm!!!
  • headaches (unwanted)

the main idea is pretty simple because:

.jenv file (fancy '.env')
    ↓
JEnvLoader (reads and parses the file)
    ↓
Map<String, String> (key-value pairs! for example: PORT=8080: PORT is the key, 8080 is the value)
    ↓
JEnv (represents the loaded configuration, woohoooooo!!!)
    ↓
JEnvValue<T> (here's a present for you!! OPEN IT.)

JEnvLoader reads and parses a JENV file. . .

JEnv provides the main configuration API. . .

JEnvValue<T> represents an individual configuration value and provides ways to safely access it or provide fallbacks. . .

JEnv, .jenv, and JENV

there are three names here and I SWEAR they are not the same thing:

  • JEnv — the name of the Java library/project
  • .jenv — the file extension
  • JENV — the configuration file format

so, for example:

development.jenv

is a .jenv file containing configuration written in the JENV format, which can be loaded using the JEnv library.

very important distinction because i absolutely would have confused myself later otherwise. how nice! (rhymes)

Features

  • loads .jenv configuration files
  • supports .jenv file paths
  • simple KEY=VALUE configuration syntax
  • ignores blank lines
  • retrieves raw values as String
  • converts values through user-provided parsers
  • JEnvValue<T> for handling present and missing values
  • Option<T> for internal optional-value representation
  • fallback values through or()
  • lazy fallback values through orElse()
  • terminal access through unwrap()
  • fallback access through unwrapOr()
  • checks for keys with contains()
  • custom JEnv exception hierarchy
  • parse errors include line and column information
  • reusable Java library API
  • Maven-based build (yet again)
  • JUnit test suite (kinda like Python's unittest, huh..)
  • does not require an application entry point because this is a library and not a program with a main() method yes?

JENV Format

JENV currently uses a deliberately simple key-value format!.

a basic file looks like:

HOST=localhost
PORT=8080
DEBUG=true

each non-blank line contains:

KEY=VALUE

the first = separates the key from the value.

values can contain additional = characters because the parser only treats the first = as the separator:

URL=https://example.com?a=b

which becomes:

key:   URL
value: https://example.com?a=b

blank lines are ignored:

HOST=localhost

PORT=8080

Loading Configuration

the main entry point for loading a configuration is:

JEnv env = JEnv.load(
    Path.of("development.jenv")
);

JEnv.load() creates a JEnvLoader, loads the specified file, and uses the resulting values to construct a JEnv instance.

the loader itself can also be used directly when the parsed map is what you want:

JEnvLoader loader = new JEnvLoader();

Map<String, String> values =
    loader.load(Path.of("development.jenv"));

this keeps file loading and configuration access separate instead of making JEnv responsible for parsing files itself..

Reading Values

given:

HOST=localhost
PORT=8080
DEBUG=true

you can retrieve a raw value with:

String host = env
    .get("HOST")
    .unwrap();

or check whether a key exists:

if (env.contains("HOST")) {
    // ...
}

missing values are represented by an empty JEnvValue:

JEnvValue<String> value =
    env.get("MISSING");

which can then be handled without immediately throwing an exception.

Typed Values

JENV files contain strings, but configuration values often represent other types.

JEnv therefore supports a parser function:

int port = env
    .get("PORT", Integer::parseInt)
    .unwrap();

and:

boolean debug = env
    .get("DEBUG", Boolean::parseBoolean)
    .unwrap();

the parser is responsible for converting the raw string into the desired type.

epic, no?

conceptually:

"8080"
   ↓
Integer::parseInt
   ↓
8080

this means JEnv doesn't need a giant collection of methods like:

getInt()
getBoolean()
getDouble()
getLong()
getFloat()
...

because the caller can provide the conversion they need. (and.. other than conversion, too!)

JEnvValue

JEnvValue<T> is the main wrapper returned by JEnv.get().

instead of immediately returning null for a missing key, JEnv represents the result explicitly:

JEnvValue<T>
├── present
└── empty

this allows operations to be chained.

for example:

String host = env
    .get("HOST")
    .or("localhost")
    .unwrap();

if HOST exists, its existing value is used.

if it doesn't exist, "localhost" is used instead.

(i did this because i have a burning love for Rust)

Lazy Defaults

orElse() accepts a Supplier<T>:

String host = env
    .get("HOST")
    .orElse(() -> "localhost")
    .unwrap();

this is useful when producing the fallback value requires some computation.

Unwrapping

unwrap() retrieves the contained value:

String host = env
    .get("HOST")
    .unwrap();

do take note that, um, this will unwrap a bomb (not all presents contain joy and glee). okay, actually, the bomb is an exception.. it DOES technically bomb the program by ending it.. so........ do something about it!!

ANYWAYS. while unwrapOr() provides a fallback at the point where the value is finally retrieved:

String host = env
    .get("HOST")
    .unwrapOr("localhost");

the difference is intentional:

or() / orElse()
    ↓
JEnvValue<T>
    ↓
unwrap() / unwrapOr()
    ↓
T

the first group continues working with JEnvValue.

the second group terminates the chain and gives you the actual value.

Option

JEnv also contains a small Option<T> abstraction for representing whether a value exists. (indeed, i am an Option<Person>. you may unwrap() me!)

it has two variants:

Option<T>
├── Some<T>
└── None<T>

for example:

Option<String> present =
    Option.some("hello");

Option<String> missing =
    Option.none();

Some cannot contain null, while None represents the absence of a value... none.. SELF-EXPLANATORY, ain't it MATE??? ok no i'm not British goodbye

back in track! Option is primarily an internal implementation detail of JEnv's value handling rather than the main public API users need to interact with.

Error Handling

JEnv has its own exception hierarchy:

RuntimeException
└── JEnvException
    └── JEnvParseException

JEnvException is the base exception for JEnv-specific errors

JEnvParseException is used when a JENV file contains invalid syntax

for example, given that:

HOST=localhost
INVALID_LINE_HAHA_UWU_NOTICE_ME_SENPAI
PORT=8080

the loader can report where the problem occurred:

Error at 2:13: Line is missing the '=' character.

the exception also exposes the location:

catch (JEnvParseException e) {
    int line = e.getLine();
    int column = e.getColumn();

    /*
    > just imagine that the code below this comment prints
    > the line, column, and error message.. yay!
    */
}

parse exceptions are not swallowed by the loader. they propagate to the caller so the application using JEnv can decide how to handle them. this is so giving ?!! (Rust reference... REFERENCE??? &&&&&&&&&&&&&&&&&&a)

Example

a complete example might look like this.

development.jenv:

HOST=localhost
PORT=8080
DEBUG=true

Java:

import java.io.IOException;
import java.nio.file.Path;

import com.jay.jenv.JEnv;

public class Main {
    public static void main(String[] args) throws IOException { // *throws trantrum*, teehee!~ JUST KIDDING OMG
        JEnv env = JEnv.load(
            Path.of("development.jenv")
        );

        String host = env
            .get("HOST")
            .unwrap();

        int port = env
            .get("PORT", Integer::parseInt)
            .unwrapOr(8080);

        boolean debug = env
            .get("DEBUG", Boolean::parseBoolean)
            .unwrapOr(false);

        System.out.println(host);
        System.out.println(port);
        System.out.println(debug);
    }
}

which gives:

localhost
8080
true

Project Structure (Base / Initial)

ooo first time doing this and, uh, i had to get these line symbols everywhere

jenv/
├── src/
│   ├── main/
│   │   └── java/
│   │       └── com/
│   │           └── jay/
│   │               └── jenv/
│   │                   ├── JEnv.java
│   │                   ├── JEnvValue.java
│   │                   │
│   │                   ├── option/
│   │                   │   └── Option.java
│   │                   │
│   │                   ├── loader/
│   │                   │   └── JEnvLoader.java
│   │                   │
│   │                   └── exception/
│   │                       ├── JEnvException.java
│   │                       └── JEnvParseException.java
│   │
│   └── test/
│       └── java/
│           └── com/
│               └── jay/
│                   └── jenv/
│                       ├── JEnvTest.java
│                       ├── JEnvValueTest.java
│                       │
│                       ├── option/
│                       │   └── OptionTest.java
│                       │
│                       └── loader/
│                           └── JEnvLoaderTest.java
│
├── .gitignore
├── LICENSE
├── README.md
└── pom.xml

the project is intentionally split into small components instead of putting loading, parsing, value handling, and configuration access into one enormous class. tada!!

Requirements

  • Java 26
  • Maven 3.9 or newer or whatever.. just, um, use a compatible Maven version?!?
  • a Java compiler compatible with the configured Java release. compatible compatible this that these those bla bla bla

Development

clone the repository:

git clone https://github.com/jayywashere/jenv
cd jenv

run the test suite:

mvn test

clean the project:

mvn clean

build the library:

mvn clean package

the compiled JAR will be placed in:

target/

you can also install the library into your local Maven repository:

mvn install

this is useful when testing JEnv from another local Maven project before publishing it publicly. wwwwwwwwww

Testing

JEnv uses JUnit for unit testing.!?

the test suite covers the main pieces of the library:

JEnv
├── value retrieval
├── typed value parsing
├── key existence
└── loading

JEnvValue
├── present values
├── empty values
├── fallbacks
└── unwrapping

Option
├── Some
├── None
└── fallback behavior

JEnvLoader
├── valid files
├── blank lines
├── malformed lines
├── invalid extensions
└── parse error locations

run all tests with:

mvn test

Why I Made This

this started as my final Java project for my little five-language challenge thing.

i wanted the Java project to be something that felt different from my usual small programs, so instead of making another CLI with:

read input
do something
print result

NOT THIS AGAIN.. how about:

read input
do something crazy
print result

oh... well, whatever... so, um:

i decided to make an actual reusable library.

i also wanted to practice Java features that are much easier to ignore when making tiny programs:

  • generics
  • functional interfaces
  • sealed interfaces
  • records
  • custom exceptions
  • file I/O
  • Maven
  • unit testing
  • API design
  • package structure

the JENV format itself is intentionally simple. i didn't want to spend 400 years inventing a configuration language with 700 features when the interesting part for me was designing the Java library around it.

there are already plenty of configuration solutions for Java.

i mostly wanted to make my own small one, understand every part of it, and have something reusable at the end.

and now apparently i have a tiny configuration format, a loader, an option abstraction, an exception hierarchy, a value wrapper, tests, and a Maven project. wowowow

uhhh.

okay.

vye i mean bye

License

See LICENSE.

About

a tiny Java library for loading '.jenv' config files because, uhmm, Java needed another way to deal with configuration??.... AND i'm totally not competing with '.env' (if i were, i'd be doomed , anyway!)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages