Open jvmlet opened 3 years ago
That's a very interesting suggestion! Could you elaborate on how you think it could work, technically?
I don't see how we could feasibly provide the bind
command to the public dummies (modules), nor do I expect it to be useful there - let me know if I'm wrong.
I do, however, cautiously see the potential in exposing a method like this:
MyPojo result = dummy4j.expressionResolver().resolveAs("#{some.map.key}", MyPojo.class);
Which would return a result with randomly generated property values.
The question, then, remains on the logic of this method. The only way I see this working is if the first argument always resolved to a Map<String, Object>
(and threw an exception otherwise), which would then be resolved to an actual object.
This begs the question, though - what library should we use for the mapping? I don't think we can use SnakeYaml for this, as the "yaml" is already a Java Map by then. Jackson ObjectMapper comes to mind, but it's yet another dependency to take on (and I don't want to bloat Dummy4j too much). We could, of course, do it by hand using reflection - the question is if it's a good idea in this case.
Moreover, how do we match the yaml properties with their POJO counterparts if they don't line up perfectly? It's tempting to expose the external library's annotations for this but, ideally, I would like that library to be replaceable and hidden behind abstractions.
It would also be useful to discuss use cases for this functionality. What is the problem you would currently like to solve with it?
I'll start from the end 😀 The problem I'm trying to solve is to get coherent dummy object, i. e. for the Android OS I want to get one of applicable versions, definitely not Vista.
You are probably right that bind API is useless for standard dummies, but it might be very handy for custom modules.
If you provide API that resolves Map<String, Object>
then Jackson mapper is the way to go. You also benefit from allowing your users to setup the custom mapping using Jackson's settings.
To make it optional, I would suggest to extract binder API to separate interface + provide default implementation on top of Jackson, but with compileOnly
dependency. Your users will have to add Jackson dependency to their project and configure binder with your default implementation if they are interested in mapping to POJOs.
If user calls bind without setting binder first you throw exception.
If they configured binder and call bind
API without adding Jackson dependency they get ClassNotFound
exception
I spent some time thinking about this and I must admit to having some second thoughts.
I think the main argument against this feature is that using the Java API to construct some objects can't be avoided (e.g. when using DateAndTimyDummy
or NumeralsDummy
). Therefore, the 'binder' couldn't be used as a true alternative to creating a dedicated "generator" class for the objects.
Moreover, at the moment I'm not sure if it would be possible to implement this functionality without modifying the ExpressionResolver
. An alternative ExpressionResolver implementation might not use a standard Map
at all and we would potentially be forcing more assumptions and functionality on it than might be needed for most people. Someone making a hypothetical cloud-based expression resolver would need to either write their own class binding logic (even if they don't need it) or throw a MethodNotImplementedException
, which suggests a Liskov violation on our part.
Thirdly, I want people to focus on using the Java API more than resolving yml keys. I think it's easier to debug actual code than it is to find errors in .yml expressions. While, of course, using Dummy4j (with its default implementations) will inevitably lead to writing .yml, I'm wary that a binder like this would encourage people to try to reimplement (or circumvent) the additional logic that is defined in Dummy4j's module code.
Finally, this is a deceptively complex feature which would potentially alter the core of what Dummy4j is and I'm not yet convinced that it would be useful enough to justify the maintenance overhead. I'm lacking an elevator pitch here.
While I'm not saying it definitely won't happen, I think I'd like to get some more people to comment on this before deciding on implementing it. At the moment, I don't think it's a good fit.
Regarding your use case, the immediate (if not entirely satisfactory) solution could be to store a hardcoded list of keys (List<String> yourList = Arrays.asList("win", "android")
) in Java, pick a random value from it (String key = dummy.of(yourList)
) and then use it in the resolver (dummy4j.expressionResolver().resolve("#{operating_system." + key + ".name"}"
).
As for a better solution, I was recently thinking about providing a way to get all keys for a given expression (e.g. dummy4j.expressionResolver().getKeysFor("operating_system")
would return ['win', 'android']
). That way you wouldn't have to hardcode anything to achieve the desired functionality. It would also be much easier to implement.
What do you think about this as an alternative to the 'binder'? Would a getKeysFor
method solve your problem?
This is how I implemented it right now.
en:
faker:
operating_system:
win:
name:
- Windows
version:
- Vista
- 7
- 8
- 10
android:
name :
- Android
version:
- Pie
- Lollilop
any:
- win
- android
String os=resolve("operating_system.any")
OperatingSystemPojo
.builder()
.name(resolve(“operating_system.“+os+".name"))
.version(resolve(“operating_system.“+os+".version"))
The locally unique API from 0.6.1—SNAPSHOT would definitely be helpful here.
If you provide API to resolve Map<String, Object>
by key I would recursively iterate it by resolving keys and populating appropriate property in pojo.
The yaml should look like this to allow both direct and random access to os:
en:
faker:
operating_system:
win: &win
name:
- Windows
version:
- Vista
- 7
- 8
- 10
android: &android
name :
- Android
version:
- Pie
- Lollilop
any:
- win:
<<: *win
- android:
<<: *android
Still, this logic is quite generic and might be provided as a binder. I can PR once you expose the required API
Resolving to Map<String, Object>
is one of the things I'd like to avoid at the moment.
What about the getKeysFor
solution? It would work somewhat like this:
For a yml file like that:
en:
operating_system:
win:
name:
- Windows
version:
- Vista
- 7
- 8
- 10
android:
name :
- Android
version:
- Pie
- Lollilop
You could do this:
List<String> operatingSystems = dummy4j.expressionResolver().getKeysFor("operating_system");
String os = dummy4j.of(operatingSystems);
OperatingSystemPojo
.builder()
.name(dummy4j.expressionResolver().resolve(“#{operating_system.“ + os + ".name}"))
.version(dummy4j.expressionResolver().resolve(“#{operating_system.“ + os + ".version}"))
.build();
Would something like this work for you?
Edit:
It's late but I just had a quick thought that you could implement a binder like this as an extension without touching Dummy4j's core and without needing to expose any Map<String, Object>
methods. Here's a quick & dirty example:
// Super quick and super dirty, but should get the point across
public class Binder {
private final Dummy4j dummy4j;
public Binder(Dummy4j dummy4j) {
this.dummy4j = dummy4j;
}
public <T> T bind(String keyPrefix, Class<T> targetClass)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
T instance = targetClass.getConstructor().newInstance();
List<Method> setters = getSetters(targetClass);
for (String fieldName : getFieldNames(targetClass)) {
Method setter = getSetter(setters, fieldName);
String lowerCaseFirstLetterFieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
setter.invoke(instance, resolveField(keyPrefix, lowerCaseFirstLetterFieldName));
}
return instance;
}
private <T> List<Method> getSetters(Class<T> targetClass) {
return Arrays.stream(targetClass.getDeclaredMethods())
.filter(m -> m.getName().startsWith("set"))
.collect(Collectors.toList());
}
private <T> List<String> getFieldNames(Class<T> targetClass) {
return Arrays.stream(targetClass.getDeclaredMethods())
.filter(m -> m.getName().startsWith("get"))
.map(m -> m.getName().substring(3))
.collect(Collectors.toList());
}
private Method getSetter(List<Method> setters, String fieldName) {
String setterName = "set" + fieldName;
return setters.stream()
.filter(s -> s.getName().equals(setterName))
.findFirst()
.orElseThrow(() -> new RuntimeException("Setter not found: " + setterName));
}
private String resolveField(String keyPrefix, String fieldName) {
return dummy4j.expressionResolver.resolve("#{" + keyPrefix + "." + fieldName + "}");
}
}
Then usage:
OperatingSystem result = binder.bind("operating_system.win", OperatingSystem.class);
You could use something like this in your code and then perhaps use the proposed getKeysFor
method to get the win/android
part of the expression.
Just thought I'd put this up here as a late-night inspiration. Still though, would a getKeysFor
method be a good enough solution for your needs, given all of the above?
I would rather load the yaml file by myself and iterate it by calling your resolve API for keys 😁 It will produce the Map<String, Object> and then I bind with single line of Jackson
Not sure I understand, but whatever gets you to whatever you need to achieve :)
In summary, I'm not going to be implementing the binding functionality at the moment. I'm still willing to consider it if you (or someone else) make a good argument for it, though.
On the other hand, I'm considering adding a getKeysFor
method which would return "all subkeys directly within a given key" (so for operating_system
that would be [win, android]
). If you think a method like that could be useful, let me know.
Should I close this issue, or do you think there is more to discuss?
I prefer to leave it opened to allow others to find it and express their opinions
Ok, I'll leave it open then.
Feel free to comment if anything else comes to your mind on this topic :)
Will be glad to hear your opinion about this