-
-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Data Docs 2.0 #331
Data Docs 2.0 #331
Changes from 8 commits
5d165b4
052a769
7eb3d9b
f6d34be
90d31de
e38e17c
51dfd0d
2fec18c
4555264
7a440d9
11be005
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
================= | ||
Data Manipulators | ||
================= | ||
|
||
Accessing and modifying data | ||
============================ | ||
|
||
A data manipulator represents a certain component and all of its data. It stores a representation of that data and can | ||
be offered to or created from data holders which possess a matching component. Again, let's use an example. And again | ||
try to heal someone (or something). | ||
|
||
**Code example: Healing with data manipulators** | ||
|
||
.. code-block:: java | ||
|
||
public static DataTransactionResult heal(DataHolder target) { | ||
Optional<HealthData> healthOptional = target.getOrCreate(HealthData.class); | ||
if (healthOptional.isPresent()) { | ||
HealthData healthData = healthOptional.get(); | ||
|
||
double maxHealth = healthData.maxHealth().get(); | ||
MutableBoundedValue<Double> currentHealth = healthData.health(); | ||
currentHealth.set(maxHealth); | ||
healthData.set(currentHealth); | ||
|
||
target.offer(healthData); | ||
} | ||
} | ||
|
||
First we need to check if our target has health data. We do so by first asking it to provide us with its health | ||
data by passing its class to the ``getOrCreate()`` method. We get an ``Optional`` which we can use for our check. | ||
If the target does not support health data, it will be absent. But if the health data is present, it now contains | ||
a mutable copy of the data present on the data holder. We make our alterations and finally offer the changed data | ||
back to our target, where it is accepted (again, ``offer`` will return a ``DataTransactionResult`` which we will | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. " |
||
just discard here). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not discard, but rather, disregarding. Even though, sometimes it should be checked for any possible errors. |
||
|
||
As you can see, the results for ``health()`` and ``maxHealth()`` are again value containers we obtain from the | ||
``DataHolder``. As the ``MutableBoundedValue`` we receive from calling ``health()`` again just contains a copy of | ||
the data, we first need to apply our changes back to the ``DataManipulator`` before we can offer the | ||
``healthData`` back to our target. | ||
|
||
.. tip:: | ||
|
||
Rule #1 of the Data API: Everything you receive is a copy. So whenever you change something, make sure that | ||
your change is propagated back to where the original value came from. | ||
|
||
|
||
DataManipulator vs. Keys | ||
======================== | ||
|
||
If you compared both of our healing examples, you may wonder 'Why bother with data manipulators anyway, keys are | ||
so much easier' and be right - for getting and setting single values. But the true merit of a data manipulator is | ||
that it contains *all* data pertaining to a certain component. Let us take a look at another example. | ||
|
||
**Code Example: Swapping two data holders' health** | ||
|
||
.. code-block:: java | ||
|
||
public void swapHealth(DataHolder targetA, DataHolder targetB) { | ||
if (targetA.supports(HealthData.class) && targetB.supports(HealthData.class)) { | ||
HealthData healthA = targetA.get(HealthData.class).get(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
HealthData healthB = targetB.get(HealthData.class).get(); | ||
targetA.offer(healthB); | ||
targetB.offer(healthA); | ||
} | ||
} | ||
|
||
First we check if both targets support HealthData. If they do, we save the health of both in one variable each. We | ||
don't need to bother with ``Optional`` this time since we verified that ``HealthData`` is supported and the | ||
``getOrCreate()`` method ensures that even if no data is present, default values are generated. | ||
|
||
Then we just offer the saved health data to the *other* target, thus switching their health status with each other. | ||
|
||
This example done with ``Keys`` would be a bit longer and more complicated since we'd have to take care of each | ||
individual key by ourself. And if instead of health we swapped another data manipulator containing even more data | ||
(maybe ``InvisibilityData`` which even contains a list), we'd have a lot more work to do. But since the data | ||
holder itself takes care of all data pertaining to it, we could even modify the above function to swap arbitrary | ||
data between two holders. | ||
|
||
**Code Example: Swapping any data manipulator** | ||
|
||
.. code-block:: java | ||
|
||
public <T extends DataManipulator<?,?>> void swapData(DataHolder targetA, DataHolder targetB, Class<T> dataClass) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this will compile with generics, have you checked? |
||
if (targetA.supports(dataClass) && targetB.supports(dataClass)) { | ||
T dataA = targetA.getOrCreate(dataClass).get(); | ||
T dataB = targetB.getOrCreate(dataClass).get(); | ||
targetA.offer(dataB); | ||
targetB.offer(dataA); | ||
} | ||
} | ||
|
||
The ability to write a function that can just swap any data on a data holder with the same data on another data | ||
holder demonstrates the core design goal of the Data API: Maximum compatibility across the API. | ||
|
||
Mutable vs. Immutable Data Manipulators | ||
======================================= | ||
|
||
To every data manipulator, there is a matching ``ImmutableDataHolder``. For instance both ``HealthData`` and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comma after "instance" |
||
``ImmutableHealthData`` contain the same data, only the latter does not provide any means to make alterations to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "only" --> "but" (just sounds better to me, you may disagree) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "only the latter returns new instances when requesting modified data." |
||
the data. | ||
|
||
Conversion between mutable and immutable data manipulators is done via the ``asImmutable()`` and ``asMutable()`` | ||
methods, which each will return a copy of the data. Since the only way to obtain an immutable data manipulator | ||
from a data holder is obtaining a mutable one and then using ``asImmutable()`` in terms of processing power it | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comma after |
||
might be cheaper to only use immutable data holders if it is to be passed around. | ||
|
||
A possible use case for this would be a custom event fired when someone is healed. It should provide copies of | ||
the health data before and after, but event listeners should not be able to change them. Therefore we can write | ||
our event to only provide ``ImmutableHealthData`` instances. That way, even if third party code gets to interact | ||
with our data, we can rest assured that it will not be changed. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
============ | ||
The Data API | ||
============ | ||
|
||
Overview | ||
======== | ||
|
||
The unified Data API aims to provide a consistent way of accessing and modifying data of any kind. Where other | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brain dump on defining "data": DataAny type of data that is consistently synchronized from the game server to the clients. If it can be changed and is synchronized to the connected clients, that is Data that the Data API should cover. Properties on the other hand, are data that can not be synchronized from the server to the client throughout the lifetime of the game, without requiring modifications to both the server and the client. Properties are explained in "insert article on properties here". |
||
approaches define the available data using interface and inheritance (like a ``LivingEntity`` interface providing | ||
getter and setter functions for current and maximum health), in Sponge every entity, block etc. is completely | ||
oblivious to what data it holds. While this may appear less straightforward than direct accessor methods, it is | ||
foremost far more extensible. And thanks to the addition of ``Key``\ s, simply accessing specific values is no | ||
less straightforward. | ||
|
||
.. warning:: | ||
|
||
As of writing, the Data API is not yet fully implemented. Refer to the `Implementation Tracker | ||
<https://github.com/SpongePowered/SpongeCommon/issues/8>`_, ask in the ``#spongedev`` IRC channel or on the | ||
`Forums <https://forums.spongepowered.org>`_ to find out if the data you need to work with is available yet. | ||
|
||
Concepts | ||
======== | ||
|
||
On first glance at the API docs, the data API threatens to overwhelm you with lots of interfaces and packages. But | ||
to simply use the data API, you will not have to deal with many of them, as most interfaces found there are just | ||
specific data manipulators. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should likely also include the notion of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If they are the backend, should they really get included into the basic section? I tried to include only those which a plugin developer will directly interact with. Are |
||
DataHolder | ||
~~~~~~~~~~ | ||
|
||
A data holder is just that - something that holds data. It provides methods to retrieve and offer back data. The | ||
interface itself is completely oblivious to the type of data held. Since only the implementations will possess | ||
this knowledge, it is possible to ask a ``DataHolder`` to provide data it does not have or to accept data it | ||
cannot use. In those cases, the return values of the methods will provide the information that data is not | ||
available (via ``Optional.absent()``) or not accepted. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or |
||
|
||
Property | ||
~~~~~~~~ | ||
|
||
A property is a read-only point of data. It provides information that is directly derived from its holders type, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "holders" --> "holder's" |
||
but, unlike other data, unchangeable (except for core mods). Example for this are the harvesting ablities on tools | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add "it is" before unchangeable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Example for this are" --> "Examples of this include" |
||
(represented as ``HarvestingProperty``) or the damage absorption of an equippable armor item. | ||
|
||
DataManipulator | ||
~~~~~~~~~~~~~~~ | ||
|
||
A data manipulator represents points of cohesive data that describes a certain component of its holder. For | ||
example ``HealthData``, which contains both current and maximum health. If a data holder has ``HealthData``, it | ||
has health that can somehow be depleted and replenished and can die if that health is depleted. This allows for | ||
the re-use of such components over the API and prevents duplication of accessor methods - as sheep, stained glass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Starting with "... - as sheep..." is worded a bit weirdly... Maybe put a period after "methods" and say "For example, sheep, stained blocks, and leather armor..." |
||
blocks and leather armor all can share the ``DyeableData`` holding the color they are dyed in. | ||
|
||
Key | ||
~~~ | ||
|
||
A ``Key`` is a unique identifier for a single point of data and can be used to directly read or set that point of | ||
data without worrying about data manipulators. It was designed to provide a convenient way of accessing data | ||
similar to direct getter/setter methods. All keys used within Sponge are listed as constants in the | ||
``org.spongepowered.api.data.key.Keys`` utility class. | ||
|
||
Value | ||
~~~~~ | ||
|
||
Within the Data API, a value referred to by a ``Key`` is encoded in a container object. For this documentation, it | ||
is referred to as 'value container' to avoid confusion with the actual value, but note that the class name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should likely find a different word to really avoid confusion as to which |
||
``ValueContainer`` has another meaning in the API. A value container encapsulates the actual data value (if it is | ||
present), a default value (to be used if no direct value is present) and the Key by which the value is identified. | ||
|
||
Contents | ||
======== | ||
|
||
.. toctree:: | ||
:maxdepth: 2 | ||
:titlesonly: | ||
|
||
keys | ||
datamanipulators | ||
transactions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
========== | ||
Using Keys | ||
========== | ||
|
||
Getting and offering data using a key | ||
===================================== | ||
|
||
Since accessing even a single point of data going from ``DataHolder`` to the correct ``DataManipulator`` tended to | ||
be quite tedious and bloat both the code and the import list, a more direct way of accessing values via ``Key``\ s | ||
was devised. Let's just start out with an example. | ||
|
||
**Code Example: Healing a data holder, if possible** | ||
|
||
.. code-block:: java | ||
|
||
import org.spongepowered.api.data.DataHolder; | ||
import org.spongepowered.api.data.key.Keys; | ||
|
||
public void heal(DataHolder target) { | ||
if (target.supports(Keys.HEALTH)) { | ||
double maxHealth = target.getOrNull(Keys.MAX_HEALTH); | ||
target.offer(Keys.HEALTH, maxHealth); | ||
} | ||
} | ||
|
||
Now for the details of the above function. | ||
|
||
The first line checks if our given data holder possesses health. Only if he does, he can be healed after all. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "he" --> "it" |
||
Since a data holder can not have health without having a maximum health and vice versa, a check for one of the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure that this logic makes sense... Maybe "Since any particular data holder is not aware of it's data types, check if it the key is applicable to the data holder using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically, since |
||
keys using the ``supports()`` method suffices. | ||
|
||
The second line uses the ``getOrNull()`` function to ask the data holder for its maximum health. Besides | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you mean |
||
``getOrNull()``, the methods ``get()`` and ``getOrElse()`` exist, all of which accept a ``Key`` as their first | ||
parameter. Generally, ``get()`` should be used. It will return an ``Optional`` of the data requested or | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Generally, |
||
``Optional.absent()`` if the data holder does not support the supplied key. The ``getOrNull()`` method we used is | ||
no more than a shortcut for ``get(key).orNull()``. In our example it is safe to use ``getOrNull()`` since we | ||
already verified that the value will be present in the first line and therefore ``getOrNull()`` will just relieve | ||
us of the ``Optional``. The third possibility would be the ``getOrElse()``, which accepts a default value as a | ||
second parameter to be returned if the value is not present on the data holder. | ||
|
||
In the third line, we offer data back to the data holder. We provide a ``Key`` denoting the current health and the | ||
before acquired maximum health, thus healing the data holder to full health. There are a variety of ``offer()`` | ||
methods accepting different parameter sets, all of which return a ``DataTransactionResult`` containing information | ||
if the offer was accepted. For now, we'll use the one accepting a ``Key`` and a corresponding value, but we will | ||
encounter more in the next pages. Since we already know that our offer of current health is accepted (as the data | ||
holder supports it), we can silently discard the result. | ||
|
||
Transforming Data | ||
================= | ||
|
||
Other than getting a value, modify it and offer it back, there is another way of modifying data. Using a data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Other than getting a value, modify it and offer it back" --> "Other than getting, modifying, and offering a value" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "modifiying" --> "interacting with" |
||
holders ``transform()`` method we can pass a ``Key`` and a ``Function``. Internally, the value for the key will | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "holders" --> "holder's" |
||
retrieved and the given function applied to it. The result is then stored under the key and the ``transform()`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "retrieved" --> "be retrieved" |
||
method will return a ``DataTransactionResult`` accordingly. | ||
|
||
Now, as an example, imagine we want to buff a data holder by doubling his maximum health. | ||
|
||
.. code-block:: java | ||
|
||
public void buff(DataHolder target) { | ||
target.transform(Keys.MAX_HEALTH, new Function<Double,Double>() { | ||
@Override | ||
public Double apply(Double input) { | ||
return (input == null) ? 0 : input * 2; | ||
} | ||
}); | ||
} | ||
|
||
Or, since we use Java 8 and are able to make use of its lambda expressions: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Or if you use Java 8, you're able to shorten the line with lambda expressions:" |
||
|
||
.. code-block:: java | ||
|
||
public void buff(DataHolder target) { | ||
target.transform(Keys.MAX_HEALTH, d -> (d == null) ? 0 : 2*d); | ||
} | ||
|
||
Note that in both cases we need to make sure our passed function can handle ``null``. You will also notice that no | ||
check has been performed if the target actually supports the ``MAX_HEALTH`` key. If a target does not support it, | ||
the ``transform()`` function will fail and return a ``DataTransactionResult`` indicating so. | ||
|
||
Value Containers | ||
================ | ||
|
||
There are cases where you may care about not only the direct value for a Key, but the value container | ||
encapsulating it. In that case, use the ``getValue(key)`` method instead of ``get(key)``. You will receive an | ||
object inheriting from ``BaseValue`` which contains a copy of the original value. Since we know that current | ||
health is a ``MutableBoundedValue``, we can find out what is the minimum possible value and set our target's | ||
health just a tiny bit above that. | ||
|
||
**Code example: Bring a target to the brink of death** | ||
|
||
.. code-block:: java | ||
|
||
public void scare(DataHolder target) { | ||
if (target.supports(Keys.HEALTH)) { | ||
MutableBoundedValue<Double> health = target.getValue(Keys.HEALTH).get(); | ||
double nearDeath = health.getMinValue() + 1; | ||
health.set(nearDeath); | ||
target.offer(health); | ||
} | ||
} | ||
|
||
Again, we check if our target support the health key and then obtain the value container. A | ||
``MutableBoundedValue`` contains a ``getMinValue()`` method, so we obtain the minimal value, add 1 and then set | ||
it to our data container. Internally, the ``set()`` method performs a check if our supplied value is valid and | ||
silently fails if it is not. Calling ``health.set(-2)`` would not change the value within ``health`` since it | ||
would fail the validity checks. To finally apply our changes to the target, we need to offer the value container | ||
back to it. As a value container also contains the ``Key`` used to identify it, calling ``target.offer(health)`` | ||
is equivalent to ``target.offer(health.getKey(), health.get())``. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
============ | ||
Transactions | ||
============ | ||
|
||
Reading the Result | ||
================== | ||
|
||
For everything you ``offer`` to a data holder, the ``offer`` method will yield a ``DataTransactionResult``. This | ||
object will contain the following: | ||
|
||
Type | ||
~~~~ | ||
|
||
The ``Type`` indicates whether the transaction was completed successfully. The following values are possible: | ||
|
||
+---------------+----------------------------------------------------------------------------+ | ||
| ``UNDEFINED`` | No clear result for the transaction - indicates that something went wrong | | ||
+---------------+----------------------------------------------------------------------------+ | ||
| ``SUCCESS`` | Transaction was completed successfully | | ||
+---------------+----------------------------------------------------------------------------+ | ||
| ``FAILURE`` | Transaction failed for expected reasons (e.g. incompatible data) | | ||
+---------------+----------------------------------------------------------------------------+ | ||
| ``ERROR`` | Transaction failed for unexpected reasons | | ||
+---------------+----------------------------------------------------------------------------+ | ||
| ``CANCELLED`` | An event for this transaction was cancelled | | ||
+---------------+----------------------------------------------------------------------------+ | ||
|
||
A ton of immutable data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this heading is needed? Not sure what it's for. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually left it as a placeholder and then forgot to change it... |
||
~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The result also provides a couple of lists containing immutable value containers | ||
|
||
+-------------------------+---------------------------------------------------------------+ | ||
| ``getSuccessfulData()`` | contains all data that was successfully set | | ||
+-------------------------+---------------------------------------------------------------+ | ||
| ``getReplacedData()`` | contains all data that got replaced by successfully set data | | ||
+-------------------------+---------------------------------------------------------------+ | ||
| ``getRejectedData()`` | contains all data that could not be set | | ||
+-------------------------+---------------------------------------------------------------+ | ||
|
||
Examples | ||
======== | ||
|
||
Healing a Player | ||
~~~~~~~~~~~~~~~~ | ||
|
||
Surely you remember the healing example in the :doc:`keys` page. Imagine a player who is down to half a heart | ||
(which equals 1 health) being healed that way. The ``DataTransactionResult`` in that case would look like this: | ||
|
||
- ``getType()`` would return ``SUCCESS`` | ||
- ``getRejectedData()`` would be an empty list | ||
- ``getReplacedData()`` would contain one value container for the ``Keys.HEALTH`` key with a value of 1.0 | ||
- ``getSuccessfulData()`` would contain one value container for the ``Keys.HEALTH`` key with a value of 20.0 | ||
|
||
Now what would be different if we used the healing example from the :doc:`datamanipulators` page instead? Since | ||
the ``HealthData`` data manipulator contains values for both the current and the maximum health, in addition to | ||
the above result, both the ``getReplacedData()`` list and the ``getSuccessfulData()`` list would contain one more | ||
element: A value container for the ``Keys.MAX_HEALTH`` key with a value of 20.0. | ||
|
||
Offering HealthData to a block of stone | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Now our above-mentioned examples are coded in a Way that they will fail silently rather than try to offer the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Way" --> "such a way" |
||
incompatible data. But imagine we took a (fully healed) player's ``HealthData`` and tried to offer it to the | ||
``Location`` of the stone block he's currently standing on. We can do this, since ``Location`` is also a data | ||
holder. And if we do, it would reward us with a ``DataTransactionResult`` like this: | ||
|
||
- ``getType()`` would return ``FAILURE`` | ||
- ``getRejectedData()`` would contain two value containers for the ``HEALTH`` and ``MAX_HEALTH`` keys, each with a value of 20.0 | ||
- ``getReplacedData()`` and ``getSuccessfulData()`` would be empty lists | ||
|
||
Reverting Transactions | ||
====================== | ||
|
||
Since everything about a transaction result is immutable, it can serve for documentation of data changes. And it | ||
also allows for those changes it documents to be undone. For that, simply pass a transaction result to the data | ||
holder's ``undo()`` method. This is particularly useful since some data offerings may be partially successful, so | ||
that one or more values are successfully written to the data holder, yet one more value cannot be accepted. Since | ||
you may wish to undo the partial successes. | ||
|
||
**Code example: Reverting a transaction** | ||
|
||
.. code-block:: java | ||
|
||
public void safeOffer(DataHolder target, DataManipulator data) { | ||
DataTransactionResult result = target.offer(data); | ||
if (result.getType() != DataTransactionResult.Type.SUCCESS) { | ||
target.undo(result); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to include that if the data is "supported" but not available, it is still created. (the whole point of
getOrCreate()
versusget()
).