Skip to content

Commit

Permalink
Merge pull request #3 from lukaszbudnik/new-shorter-syntax
Browse files Browse the repository at this point in the history
v1.3.0
  • Loading branch information
lukaszbudnik committed Sep 19, 2015
2 parents 312bc00 + b8e881d commit 2038436
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 34 deletions.
49 changes: 35 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
jelvis [![Build Status](https://travis-ci.org/lukaszbudnik/jelvis.svg?branch=master)](https://travis-ci.org/lukaszbudnik/jelvis) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.lukaszbudnik.jelvis/jelvis/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/com.github.lukaszbudnik.jelvis/jelvis)
==============================

Elvis operator in Java 8 & Scala! Say no to NPE in chained calls!
jelvis is an elvis operator for Java 8 & Scala. jelvis eats `NullPointerException` in chained calls and returns null instead.

For example in this chained call:

```
person.getAddress().getCountry()
```

you can get at least two NPEs: first when `person` is null and second when `getAddress()` returns null.

jelvis takes care of NPEs for you. For the above call jelvis will return the value of `getCountry()` or null if either `person` or `getAddress()` is null. No NPE will be thrown.

# Getting started

## Java 8

1. Add jelvis to your project
2. Use the static `elvis` method like this:
All you need is just one static import:

```
import static com.github.lukaszbudnik.jelvis.Elvis.elvis;
//...
Person person = new Person();
// old syntax requires 2 arguments
// the first argument to `elvis` method is the root object
// the second one is the lambda function to be evaluated
String isoCode = elvis(person, p -> p.getAddress().getCountry().getISOCode());
```

The first argument to `elvis` method is the root object and the second one is the function to be evaluated.
If either `person` is null or `getAddress()` or `getCountry()` returns null, the whole call returns null.
// new syntax takes just a lambda to be evaluated
// a little bit shorter than the old syntax.
String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode());
```

No `NullPointerException` is thrown. All other exceptions are preserved.
However, Java 8 lambdas have some problems with functions throwing checked exceptions.
If your methods throw checked exception you need to use `wrappedFunction` like this:
Java 8 lambdas have some problems with functions throwing checked exceptions (code simply does not compile).
To overcome this limitation jelvis is wrapping all checked exceptions into a runtime exception: `ElvisException`.

Prior to jelvis 1.3 you had to explicitly wrap functions throwing exceptions using `Elvis.wrappedFunction()`.
This is now done automatically:

```
import static com.github.lukaszbudnik.jelvis.Elvis.elvis;
import static com.github.lukaszbudnik.jelvis.Elvis.wrappedFunction;
//...
Person person = new Person();
String line2 = elvis(person, wrappedFunction(p -> p.getAddress().getLine2()));
// starting with jelvis 1.3 there is no need to use Elvis.wrappedFunction()
// old syntax
String line2 = elvis(person, p -> p.getAddress().getLine2());
// new syntax
String line2 = elvis(() -> person.getAddress().getLine2());
```

## Scala

There is an implicit converter which converts Scala `Function1` into Java 8 `Function`.
All you have to do is import it in your code:
There is an implicit converter which converts Scala `Function1` into jelvis functions.
So in case of Scala you have to add two imports:

```
import com.github.lukaszbudnik.jelvis.Elvis._
Expand All @@ -47,11 +65,14 @@ And then just use the `elvis` method in your code:

```
val person = new Person
// old syntax
val isoCode = elvis(person, (p: Person) => p.getAddress.getCountry.getISOCode)
// new syntax
val isoCode = elvis(() => person.getAddress.getCountry.getISOCode)
```

Exceptions? In Scala all exceptions are unchecked. You don't have to worry about them at all.
For example `p.getAddress.getLine2` doesn't have to be wrapped with `wrappedFunction`.
For example `p.getAddress.getLine2` doesn't have to be wrapped using `Elvis.wrappedFunction()`.
Thus, when running from Scala, you will never see `ElvisException`.
The call simply looks like this:

Expand Down
5 changes: 2 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ targetCompatibility = JavaVersion.VERSION_1_8

group = 'com.github.lukaszbudnik.jelvis'
archivesBaseName = 'jelvis'
version = '1.2'
version = '1.3.0'

repositories {
mavenCentral()
Expand Down Expand Up @@ -119,8 +119,7 @@ uploadArchives {
pom.project {
name 'jelvis'
packaging 'jar'
// optionally artifactId can be defined here
description 'Elvis operator in Java 8!'
description 'Elvis operator for Java 8 and Scala'
url 'https://github.com/lukaszbudnik/jelvis'

scm {
Expand Down
33 changes: 28 additions & 5 deletions src/main/java/com/github/lukaszbudnik/jelvis/Elvis.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,22 @@ public final class Elvis {
private Elvis() {
}

public static <T, R> R elvis(T t, Function<T, R> f) {
public static <R> R elvis(NoArgFunctionThrowingException<R> f) {
try {
log.trace("About to apply no arg function " + f);
NoArgFunction<R> wrappedFunction = wrappedFunction(f);
return wrappedFunction.apply();
} catch (NullPointerException e) {
log.debug("NullPointerException caught - gracefully returning null instead", e);
return null;
}
}

public static <T, R> R elvis(T t, FunctionThrowingException<T, R> f) {
try {
log.trace("About to apply object " + t + " on function " + f);
return f.apply(t);
Function<T, R> wrappedFunction = wrappedFunction(f);
return wrappedFunction.apply(t);
} catch (NullPointerException e) {
log.debug("NullPointerException caught - gracefully returning null instead", e);
return null;
Expand All @@ -47,9 +59,20 @@ public static <T, R> Function<T, R> wrappedFunction(FunctionThrowingException<T,
};
}

@FunctionalInterface
public interface FunctionThrowingException<T, R> {
R apply(T t) throws Exception;
public static <R> NoArgFunction<R> wrappedFunction(NoArgFunctionThrowingException<R> f) {
log.trace("Wrapping function " + f);
return () -> {
try {
log.trace("Delegating non-arg function " + f);
return f.apply();
} catch (RuntimeException e) {
log.debug("RuntimeException caught - re-throwing as is", e);
throw e;
} catch (Exception e) {
log.debug("Checked Exception caught - wrapping into ElvisException and re-throwing", e);
throw new ElvisException("Checked exception was thrown", e);
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (C) 2015 Łukasz Budnik <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
package com.github.lukaszbudnik.jelvis;

@FunctionalInterface
interface FunctionThrowingException<T, R> {
R apply(T t) throws Exception;
}

@FunctionalInterface
interface NoArgFunctionThrowingException<R> {
R apply() throws Exception;
}

@FunctionalInterface
interface NoArgFunction<R> {
R apply();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
*/
package com.github.lukaszbudnik.jelvis

import java.util.function.{Function => JFunction}

object ElvisScalaToJavaConverters {

implicit def toJavaFunction[A, B](f: Function1[A, B]) = new JFunction[A, B] {
override def apply(a: A): B = f(a)
implicit def toJavaFunction[T, R](f: Function1[T, R]) = new FunctionThrowingException[T, R] {
override def apply(t: T): R = f(t)
}

def fromScala: Boolean = true
implicit def toJavaFunction[R](f: Function0[R]) = new NoArgFunctionThrowingException[R] {
override def apply(): R = f()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Copyright (C) 2015 Łukasz Budnik <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
package com.github.lukaszbudnik.jelvis;

import com.github.lukaszbudnik.jelvis.Model.Address;
import com.github.lukaszbudnik.jelvis.Model.Person;
import org.junit.Assert;
import org.junit.Test;

import static com.github.lukaszbudnik.jelvis.Elvis.elvis;

public class ElvisNewSyntaxTest {

@Test
public void shouldReturnNotNullWhenAllIsGood() {
Person person = new Person();
Address address = elvis(() -> person.getAddress());

Assert.assertNotNull(address);
}

@Test
public void shouldReturnNotNullWhenAllIsGoodWrappedFunction() {
Person person = new Person();
int iq = elvis(() -> person.getIQ());

Assert.assertEquals(100, iq);
}

@Test
public void shouldReturnNullWhenChainedGetterReturnsNull() {
Person person = new Person();
String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode());

Assert.assertNull(isoCode);
}

@Test
public void shouldReturnNullWhenRootObjectIsNull() {
Person person = null;
String isoCode = elvis(() -> person.getAddress().getCountry().getISOCode());

Assert.assertNull(isoCode);
}

@Test
public void shouldThrowOriginalRuntimeException() {
Person person = new Person();

try {
elvis(() -> person.getAddress().getLine1());
Assert.fail();
} catch (RuntimeException e) {
Assert.assertEquals("getLine1 threw runtime exception", e.getMessage());
} catch (Throwable t) {
Assert.fail();
}
}

@Test
public void shouldThrowElvisExceptionForCheckedExceptions() {
Person person = new Person();

try {
elvis(() -> person.getAddress().getLine2());
Assert.fail("Exception was expected");
} catch (ElvisException e) {
Assert.assertEquals("getLine2 threw checked exception", e.getCause().getMessage());
} catch (Throwable t) {
Assert.fail("Throwable was not expected");
}
}

@Test
public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedException() {
Person person = new Person();

try {
elvis(() -> person.getAddress().getGeoLocation());
Assert.fail("Exception was expected");
} catch (RuntimeException e) {
Assert.assertEquals("Geo location declares Exception but is throwing runtime exception", e.getMessage());
} catch (Throwable t) {
Assert.fail("Throwable was not expected");
}
}

}
8 changes: 3 additions & 5 deletions src/test/java/com/github/lukaszbudnik/jelvis/ElvisTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.junit.Test;

import static com.github.lukaszbudnik.jelvis.Elvis.elvis;
import static com.github.lukaszbudnik.jelvis.Elvis.wrappedFunction;

public class ElvisTest {

Expand All @@ -30,7 +29,7 @@ public void shouldReturnNotNullWhenAllIsGood() {
@Test
public void shouldReturnNotNullWhenAllIsGoodWrappedFunction() {
Person person = new Person();
int iq = elvis(person, wrappedFunction(p -> p.getIQ()));
int iq = elvis(person, p -> p.getIQ());

Assert.assertEquals(100, iq);
}
Expand Down Expand Up @@ -70,7 +69,7 @@ public void shouldThrowElvisExceptionForCheckedExceptions() {
Person person = new Person();

try {
elvis(person, wrappedFunction(p -> p.getAddress().getLine2()));
elvis(person, p -> p.getAddress().getLine2());
Assert.fail("Exception was expected");
} catch (ElvisException e) {
Assert.assertEquals("getLine2 threw checked exception", e.getCause().getMessage());
Expand All @@ -84,7 +83,7 @@ public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedExcep
Person person = new Person();

try {
elvis(person, wrappedFunction(p -> p.getAddress().getGeoLocation()));
elvis(person, p -> p.getAddress().getGeoLocation());
Assert.fail("Exception was expected");
} catch (RuntimeException e) {
Assert.assertEquals("Geo location declares Exception but is throwing runtime exception", e.getMessage());
Expand All @@ -94,4 +93,3 @@ public void shouldThrowRuntimeExceptionCorrectlyEvenIfMethodDeclaresCheckedExcep
}

}

14 changes: 12 additions & 2 deletions src/test/scala/com/github/lukaszbudnik/jelvis/ElvisSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.github.lukaszbudnik.jelvis.Elvis._
import com.github.lukaszbudnik.jelvis.ElvisScalaToJavaConverters._
import com.github.lukaszbudnik.jelvis.Model._
import org.junit.runner.RunWith
import org.specs2.matcher.{MatchResult, Expectable, Matcher}
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner

Expand All @@ -29,6 +30,14 @@ class ElvisSpec extends Specification {
iq must beEqualTo(100)
}

"return expected values when all is good - using new syntax" in {
val person: Person = new Person

val iq: Int = elvis(() => person.getIQ)

iq must beEqualTo(100)
}

"return null when chained call throws NPE" in {
val person = new Person
val isoCode = elvis(person, (p: Person) => p.getAddress.getCountry.getISOCode)
Expand All @@ -39,9 +48,10 @@ class ElvisSpec extends Specification {
"throw ElvisException when chained call throws checked exception" in {
val person = new Person

elvis(person, (p: Person) => p.getAddress.getLine2) must throwAn[Exception]("getLine2 threw checked exception")
elvis(person, (p: Person) => p.getAddress.getLine2) must throwAn[Exception].like {
case e: ElvisException => e.getCause.getMessage must equalTo("getLine2 threw checked exception")
}
}

}

}

0 comments on commit 2038436

Please sign in to comment.