Change default behavior of `emptyResultThrowsException` to `false`, update `ResponseValidator` for improved handling of single and multiple results, and enhance test coverage and documentation.

Add Fluent API for DNS operations, update `README`, and refactor `CfDnsClient` for zone-level chaining.

Validate record type in `RecordEntity#build`, throw exception for invalid types, and add test coverage.

Refactor `RecordEntity#getSld` to handle `zoneName` cases, improve null safety, and enhance substring logic.

Refactor `RecordEntity` getter (`getName` → `getSld`) for clarity, improve string handling in batch processing, and enhance type safety in HTTP operations. Update tests and documentation accordingly.

Refactor `deleteRequest` and `patchRequest` to accept `responseType` parameter for improved flexibility. Extract methods for cleaning and processing batch DNS records.

Refactor API method names for consistency (`zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` → `recordList`, `sldInfo` → `recordGet`) and update related documentation and tests.
This commit is contained in:
2026-01-05 17:59:26 +01:00
parent 1bfea09aa9
commit a221de4792
14 changed files with 760 additions and 159 deletions
@@ -102,10 +102,16 @@ abstract class CfBasicHttpClient {
/**
* Sends a DELETE request to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T deleteRequest(String endpoint) throws CloudflareApiException {
<T extends AbstractResponse> T deleteRequest(String endpoint, Class<T> responseType) throws CloudflareApiException {
HttpDelete request = new HttpDelete(buildUrl(endpoint));
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
return executeRequest(request, responseType);
}
@@ -135,13 +141,21 @@ abstract class CfBasicHttpClient {
/**
* Sends a PATCH request with a payload to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param requestPayload the payload to send
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T patchRequest(String endpoint,
Object requestPayload)
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPatch request = new HttpPatch(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
return executeRequest(request, responseType);
}
/**
@@ -1,5 +1,7 @@
package codes.thischwa.cf;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.BatchResponse;
@@ -12,7 +14,6 @@ import codes.thischwa.cf.model.ZoneEntity;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.util.ArrayList;
import java.util.List;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
@@ -29,19 +30,18 @@ import org.jetbrains.annotations.Nullable;
* "yourApiKey"
* );
* // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneInfo("example.com");
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId());
* // Retrieve records of a subdomain
* List&lt;{@link RecordEntity}&gt; records = cfDnsClient.sldListAll(zone, "sld");
* List&lt;{@link RecordEntity}&gt; records = cfDnsClient.recordGet(zone, "sld");
* records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* );
* // Create a record for the subdomain "api"
* RecordEntity created = client.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.10);
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId());
* </code></pre>
*/
@Setter
@Slf4j
public class CfDnsClient extends CfBasicHttpClient {
private static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
@@ -70,15 +70,15 @@ public class CfDnsClient extends CfBasicHttpClient {
* process.
*/
public CfDnsClient(String baseUrl, String authEmail, String authKey) {
this(true, baseUrl, authEmail, authKey);
this(false, baseUrl, authEmail, authKey);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty, it's valid for 'list
* requests' only. Default is true.
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param authEmail The email address associated with the Cloudflare account,
* used for authentication.
* @param authKey The API key of the Cloudflare account, used as part of the
@@ -92,8 +92,8 @@ public class CfDnsClient extends CfBasicHttpClient {
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty, it's valid for 'list
* requests' only. Default is true.
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param baseUrl The base URL for the Cloudflare API endpoint.
* @param authEmail The email associated with the Cloudflare account for
* authentication.
@@ -110,14 +110,35 @@ public class CfDnsClient extends CfBasicHttpClient {
return sld + "." + zone.getName();
}
/**
* Provides fluent API access to operations on a specific zone.
* This method returns a ZoneOperations interface that allows chaining operations
* on DNS records within the specified zone.
*
* <p>Example:
* <pre><code>
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
* </code></pre>
*
* @param zoneName the name of the DNS zone (e.g., "example.com")
* @return a ZoneOperations instance for chaining operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
public ZoneOperations zone(String zoneName) throws CloudflareApiException {
ZoneEntity zoneEntity = zoneGet(zoneName);
return new ZoneOperationsImpl(this, zoneEntity);
}
/**
* Retrieves a list of all zones from the Cloudflare API.
*
* @return A list of ZoneEntity objects representing the zones retrieved from the Cloudflare API.
* @throws CloudflareApiException If an error occurs during the API request or response handling.
*/
public List<ZoneEntity> zoneListAll() throws CloudflareApiException {
return zoneListAll(PagingRequest.defaultPaging());
public List<ZoneEntity> zoneList() throws CloudflareApiException {
return zoneList(PagingRequest.defaultPaging());
}
/**
@@ -129,7 +150,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareApiException if there is an error during the API request or response
* processing
*/
public List<ZoneEntity> zoneListAll(PagingRequest pagingRequest) throws CloudflareApiException {
public List<ZoneEntity> zoneList(PagingRequest pagingRequest) throws CloudflareApiException {
String endpoint = pagingRequest.addQueryString(CfRequest.ZONE_LIST.buildPath());
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response);
@@ -144,7 +165,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareApiException If an error occurs while making the API request or processing
* the response.
*/
public ZoneEntity zoneInfo(String name) throws CloudflareApiException {
public ZoneEntity zoneGet(String name) throws CloudflareApiException {
String endpoint = CfRequest.ZONE_INFO.buildPath(name);
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response, true);
@@ -160,8 +181,8 @@ public class CfDnsClient extends CfBasicHttpClient {
* @return A list of {@code RecordEntity} associated with the desired SLD.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public List<RecordEntity> sldListAll(ZoneEntity zone, String sld) throws CloudflareApiException {
return sldListAll(zone, sld, PagingRequest.defaultPaging());
public List<RecordEntity> recordList(ZoneEntity zone, String sld) throws CloudflareApiException {
return recordList(zone, sld, PagingRequest.defaultPaging());
}
/**
@@ -174,7 +195,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @return A list of {@code RecordEntity} associated with the desired SLD.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public List<RecordEntity> sldListAll(ZoneEntity zone, String sld, PagingRequest pagingRequest)
public List<RecordEntity> recordList(ZoneEntity zone, String sld, PagingRequest pagingRequest)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint =
@@ -194,12 +215,13 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> sldInfo(ZoneEntity zone, String sld) throws CloudflareApiException {
return sldInfo(zone, sld, (RecordType[]) null);
public List<RecordEntity> recordGet(ZoneEntity zone, String sld) throws CloudflareApiException {
return recordGet(zone, sld, (RecordType[]) null);
}
/**
* Retrieves a list of DNS records for a given second-level domain (SLD) within a specific zone.
* Optionally filters by one or more DNS record types.
*
* @param zone The zone entity containing information about the domain zone.
* @param sld The second-level domain (SLD) for which to retrieve DNS records.
@@ -208,7 +230,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> sldInfo(ZoneEntity zone, String sld, @Nullable RecordType... types)
public List<RecordEntity> recordGet(ZoneEntity zone, String sld, @Nullable RecordType... types)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint = buildEndpointWithTypeFilters(zone.getId(), fqdn, types);
@@ -222,12 +244,11 @@ public class CfDnsClient extends CfBasicHttpClient {
if (types == null || types.length == 0) {
return baseEndpoint;
}
StringBuilder queryParams = new StringBuilder();
StringBuilder endpoint = new StringBuilder(baseEndpoint);
for (RecordType type : types) {
queryParams.append("&");
queryParams.append("type=").append(type);
endpoint.append("&type=").append(type);
}
return baseEndpoint + queryParams;
return endpoint.toString();
}
/**
@@ -281,7 +302,7 @@ public class CfDnsClient extends CfBasicHttpClient {
String endpoint = CfRequest.RECORD_CREATE.buildPath(zone.getId());
RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful created.", rec.getName(), rec.getType());
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
return resp.getResult();
}
@@ -297,9 +318,9 @@ public class CfDnsClient extends CfBasicHttpClient {
public boolean recordDelete(ZoneEntity zone, RecordEntity rec) throws CloudflareApiException {
boolean changed = recordDelete(zone, rec.getId());
if (changed) {
log.debug("Record {} of the type [{}] successful deleted.", rec.getName(), rec.getType());
log.debug("Record {} of the type [{}] successful deleted.", rec.getSld(), rec.getType());
} else {
log.warn("Record {} of the type [{}] was not deleted.", rec.getName(), rec.getType());
log.warn("Record {} of the type [{}] was not deleted.", rec.getSld(), rec.getType());
}
return changed;
}
@@ -315,7 +336,7 @@ public class CfDnsClient extends CfBasicHttpClient {
*/
public boolean recordDelete(ZoneEntity zone, String id) throws CloudflareApiException {
String endpoint = CfRequest.RECORD_DELETE.buildPath(zone.getId(), id);
RecordSingleResponse resp = deleteRequest(endpoint);
RecordSingleResponse resp = deleteRequest(endpoint, RecordSingleResponse.class);
checkResponse(resp);
log.debug("Record id#{} successful deleted.", id);
return resp.getResult().getId().equals(id);
@@ -336,9 +357,9 @@ public class CfDnsClient extends CfBasicHttpClient {
rec.setModifiedOn(null);
rec.setCreatedOn(null);
String endpoint = CfRequest.RECORD_UPDATE.buildPath(zone.getId(), rec.getId());
RecordSingleResponse resp = patchRequest(endpoint, rec);
RecordSingleResponse resp = patchRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful updated.", rec.getName(), rec.getType());
log.info("Record {} of type {} successful updated.", rec.getSld(), rec.getType());
return resp.getResult();
}
@@ -356,7 +377,7 @@ public class CfDnsClient extends CfBasicHttpClient {
String fqdn = buildFqdn(zone, sld);
List<RecordEntity> recs;
try {
recs = sldInfo(zone, sld, recordTypes);
recs = recordGet(zone, sld, recordTypes);
} catch (CloudflareNotFoundException e) {
log.trace("No record of type {} found for domain {}.", recordTypes, fqdn);
return;
@@ -390,27 +411,16 @@ public class CfDnsClient extends CfBasicHttpClient {
BatchEntry batchEntry = new BatchEntry();
// build 'clean' record entries
if (postRecords != null) {
List<RecordEntity> cleanedPosts = new ArrayList<>();
postRecords.forEach(
rec -> cleanedPosts.add(RecordEntity.build(rec.getId(), rec.getName(), rec.getType(), rec.getTtl(), rec.getContent())));
batchEntry.setPosts(cleanedPosts);
batchEntry.setPosts(cleanRecordsForPostOrPut(postRecords));
}
if (putRecords != null) {
List<RecordEntity> cleanedPuts = new ArrayList<>();
putRecords.forEach(
rec -> cleanedPuts.add(RecordEntity.build(rec.getId(), rec.getName(), rec.getType(), rec.getTtl(), rec.getContent())));
batchEntry.setPuts(cleanedPuts);
batchEntry.setPuts(cleanRecordsForPostOrPut(putRecords));
}
if (patchRecords != null) {
List<RecordEntity> cleanedPatches = new ArrayList<>();
patchRecords.forEach(rec -> cleanedPatches.add(RecordEntity.build(rec.getId(), rec.getContent())));
batchEntry.setPatches(cleanedPatches);
batchEntry.setPatches(cleanRecordsForPatch(patchRecords));
}
if (deleteRecords != null) {
List<RecordEntity> cleanedDeletes = new ArrayList<>();
deleteRecords.forEach(
rec -> cleanedDeletes.add(RecordEntity.build(rec.getId(), rec.getName(), rec.getType(), null, rec.getContent())));
batchEntry.setDeletes(cleanedDeletes);
batchEntry.setDeletes(cleanRecordsForDelete(deleteRecords));
}
String endpoint = CfRequest.RECORD_BATCH.buildPath(zone.getId());
@@ -419,16 +429,40 @@ public class CfDnsClient extends CfBasicHttpClient {
// set zone id
BatchEntry result = resp.getResult();
setZoneIdForBatchResults(result, zone.getId());
return result;
}
private List<RecordEntity> cleanRecordsForPostOrPut(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), rec.getTtl(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForPatch(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForDelete(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), null, rec.getContent())));
return cleaned;
}
private void setZoneIdForBatchResults(BatchEntry result, String zoneId) {
if (result.getPosts() != null) {
result.getPosts().forEach(rec -> rec.setZoneId(zone.getId()));
result.getPosts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPuts() != null) {
result.getPuts().forEach(rec -> rec.setZoneId(zone.getId()));
result.getPuts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPatches() != null) {
result.getPatches().forEach(rec -> rec.setZoneId(zone.getId()));
result.getPatches().forEach(rec -> rec.setZoneId(zoneId));
}
return result;
}
private void checkResponse(AbstractResponse resp) throws CloudflareApiException {
@@ -1,6 +1,7 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.AbstractSingleResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import java.util.stream.Collectors;
@@ -14,9 +15,11 @@ import java.util.stream.Collectors;
* <li>It checks whether the API response was successful by analyzing the associated response
* metadata. If the response indicates failure, an exception is thrown with descriptive error
* messages.
* <li>If a {@link RecordMultipleResponse} is used, it validates the number of results in the API
* response payload to detect unexpected counts. Depending on the parameter
* 'emptyResultThrowsException', an exception will be triggered or an empty result will be returned.
* <li>It validates the number of results in the API response payload to detect unexpected counts.
* For {@link RecordMultipleResponse}, it checks if results are empty or if more than one result
* was returned when a single result was expected. For {@link AbstractSingleResponse}, it checks
* if the result is null. Depending on the parameter 'emptyResultThrowsException', an exception
* will be triggered or an empty/null result will be returned.
* </ul>
*/
class ResponseValidator {
@@ -50,6 +53,10 @@ class ResponseValidator {
if (emptyResultThrowsException && respMulti.getResultInfo().totalCount() == 0) {
throw new CloudflareNotFoundException("No result found");
}
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
if (emptyResultThrowsException && respSingle.getResult() == null) {
throw new CloudflareNotFoundException("No result found");
}
}
}
@@ -0,0 +1,49 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import java.util.List;
/**
* Fluent interface for record-level operations.
* Provides a chainable API for CRUD operations on DNS records.
*/
public interface RecordOperations {
/**
* Retrieves DNS records for the selected subdomain.
*
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
List<RecordEntity> get() throws CloudflareApiException;
/**
* Creates a new DNS record with the specified parameters.
*
* @param type the DNS record type (e.g., A, AAAA, CNAME)
* @param content the content of the DNS record (e.g., IP address)
* @param ttl the time-to-live value in seconds
* @return the created RecordEntity
* @throws CloudflareApiException if an error occurs while creating the record
*/
RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException;
/**
* Updates an existing DNS record with new content.
*
* @param newContent the new content for the DNS record
* @return the updated RecordEntity
* @throws CloudflareApiException if an error occurs while updating the record
*/
RecordEntity update(String newContent) throws CloudflareApiException;
/**
* Deletes DNS records of the specified types.
*
* @param types the DNS record types to delete
* @throws CloudflareApiException if an error occurs while deleting records
*/
void delete(RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,67 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of RecordOperations for fluent API access to record-level operations.
*/
public class RecordOperationsImpl implements RecordOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
private final String sld;
private final RecordType[] types;
/**
* Constructs a RecordOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
* @param sld the subdomain (second-level domain) name
* @param types optional array of RecordType to filter by
*/
public RecordOperationsImpl(CfDnsClient client, ZoneEntity zone, String sld, @Nullable RecordType[] types) {
this.client = client;
this.zone = zone;
this.sld = sld;
this.types = types;
}
@Override
public List<RecordEntity> get() throws CloudflareApiException {
if (types == null || types.length == 0) {
return client.recordGet(zone, sld);
}
return client.recordGet(zone, sld, types);
}
@Override
public RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException {
return client.recordCreateSld(zone, sld, ttl, type, content);
}
@Override
public RecordEntity update(String newContent) throws CloudflareApiException {
List<RecordEntity> records = get();
if (records.isEmpty()) {
throw new CloudflareApiException("No records found to update for subdomain: " + sld);
}
if (records.size() > 1) {
throw new CloudflareApiException("Multiple records found. Please use recordUpdate() directly for precise control.");
}
RecordEntity record = records.get(0);
record.setContent(newContent);
return client.recordUpdate(zone, record);
}
@Override
public void delete(RecordType... types) throws CloudflareApiException {
client.recordDeleteTypeIfExists(zone, sld, types);
}
}
@@ -0,0 +1,31 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordType;
import org.jetbrains.annotations.Nullable;
/**
* Fluent interface for zone-level operations.
* Provides a chainable API for accessing and manipulating DNS records within a specific zone.
*/
public interface ZoneOperations {
/**
* Selects a record (subdomain) within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld) throws CloudflareApiException;
/**
* Selects a record with specific types within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @param types optional DNS record types to filter by
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,37 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of ZoneOperations for fluent API access to zone-level operations.
*/
public class ZoneOperationsImpl implements ZoneOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
/**
* Constructs a ZoneOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
*/
public ZoneOperationsImpl(CfDnsClient client, ZoneEntity zone) {
this.client = client;
this.zone = zone;
}
@Override
public RecordOperations record(String sld) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, null);
}
@Override
public RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, types);
}
}
@@ -0,0 +1,31 @@
/**
* Fluent API interfaces and implementations for chainable DNS operations.
*
* <p>This package provides a fluent, chainable interface for interacting with Cloudflare DNS
* records, making code more readable and concise.
*
* <p>Example usage:
* <pre><code>
* // Create a DNS record
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
*
* // Get DNS records
* List&lt;RecordEntity&gt; records = client.zone("example.com")
* .record("www", RecordType.A)
* .get();
*
* // Update a DNS record
* client.zone("example.com")
* .record("api", RecordType.A)
* .update("192.168.1.2");
*
* // Delete DNS records
* client.zone("example.com")
* .record("old-service")
* .delete(RecordType.A, RecordType.AAAA);
* </code></pre>
*/
package codes.thischwa.cf.fluent;
@@ -21,6 +21,12 @@ import lombok.Data;
*/
@Data
public class PagingRequest {
/**
* Default page size for retrieving all records in a single request.
* Set to a very high value to effectively disable pagination when fetching all records.
*/
private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 5_000_000;
private int page;
private int perPage;
@@ -42,19 +48,19 @@ public class PagingRequest {
/**
* Creates a default {@code PagingRequest} instance with a page number set to 1 and a high number
* of items per page (5,000,000) to accommodate large dataset requests.
* of items per page to accommodate large dataset requests and effectively retrieve all records.
*
* @return a default {@code PagingRequest} instance with predefined pagination parameters
*/
public static PagingRequest defaultPaging() {
return new PagingRequest(1, 5000000);
return new PagingRequest(1, DEFAULT_ALL_RECORDS_PAGE_SIZE);
}
/**
* Retrieves the pagination parameters in a key-value map format.
*
* @return a map containing the pagination parameters, where the key "page" indicates the current
* page number and the key "perPage" indicates the number of items per page.
* page number and the key "perPage" indicates the number of items per page.
*/
public Map<String, String> getPagingParams() {
return Map.of("page", String.valueOf(page), "perPage", String.valueOf(perPage));
@@ -94,27 +94,45 @@ public class RecordEntity extends AbstractEntity {
* @param ttl the time-to-live (TTL) value for the DNS record
* @param content the content of the DNS record, typically an IP address or other record data
* @return a {@link RecordEntity} populated with the provided attributes
* @throws IllegalArgumentException if the type string is not a valid RecordType
*/
public static RecordEntity build(String id, String name, String type, Integer ttl, String content) {
RecordEntity rec = build(name, RecordType.valueOf(type), ttl, content);
RecordType recordType;
try {
recordType = RecordType.valueOf(type);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e);
}
RecordEntity rec = build(name, recordType, ttl, content);
rec.setId(id);
return rec;
}
/**
* Retrieves the name of the DNS record.
* Retrieves the short name (subdomain) of the DNS record.
* If the name contains a dot ('.'), only the substring before the first dot is returned.
* This is useful for getting the subdomain part of a fully qualified domain name.
*
* @return the name of the DNS record, potentially truncated before the first dot,
* or the full name if no dot is present.
* @return the short name of the DNS record (substring before the first dot),
* or the full name if no dot is present
*/
public String getName() {
if (name != null) {
int pos = name.indexOf('.');
if (pos > 0) {
return name.substring(0, pos);
}
public String getSld() {
if (name == null) {
return null;
}
if (zoneName != null && name.endsWith(zoneName)) {
int zoneNameLength = zoneName.length();
int dotSeparatorLength = 1;
return name.substring(0, name.length() - zoneNameLength - dotSeparatorLength);
}
int firstDotPosition = name.indexOf('.');
if (firstDotPosition > 0) {
return name.substring(0, firstDotPosition);
}
return name;
}
}
}