Repository navigation
Add the simple-editor email, brand style and file endpoints - #23
tomasmartty wants to merge 35 commits into
Conversation
Add a fake HttpClient that keeps each request with its raw body and returns queued responses. New tests use it, so they send no request to the network and need no recorded tapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request methods cast an IOException or an InterruptedException to MailerLiteException. The cast failed, so callers got a ClassCastException. Now the methods throw a MailerLiteException with the network error as the cause. For an InterruptedException, the methods also set the interrupt flag of the thread again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add MailerLiteApi.sendRequest. It sends any HTTP method, for example PATCH, with an optional body and extra headers. It sends a DELETE body only when the caller gives one. When the body is null, a POST or PATCH has an empty body, not the JSON null. Each request has the headers Content-Type and Accept with the value application/json. The old request methods do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the fields errorCode and etag, and the method isVersionConflict(), to MailerLiteException. The field code stays the HTTP status. When the API sends a code, the message starts with the code, for example "version_conflict: ...". A body that is not JSON keeps the old message. Add EmailErrorCodes.VERSION_CONFLICT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApiRequest sent query values as they were. A space made the URI invalid, and a null value caused a NullPointerException. Now the SDK encodes each value as UTF-8, so a space is sent as "+". A null value is not sent, and it removes a value that was set before. An empty value is sent as "name=". The keys do not change, so "filter[name]" keeps its brackets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RequestChecks with checks for IDs, etags and idempotency keys. An ID must contain only the ASCII digits 0 to 9. The checks throw EmptyIdException, InvalidIdException, EmptyEtagException or InvalidIdempotencyKeyException. All four extend IllegalArgumentException. Add EditorContentAdapter for editor_content maps. It reads whole numbers as Long, so 7 is not sent back as 7.0. It also keeps the null values in the map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the Emails service for emails built with the simple editor, and the accessor MailerLite.emails(). Emails.get returns the email with its design settings, its blocks and its etag. Add the models of the email, the blocks and the design settings. The editor_content maps use EditorContentAdapter. Add a test that compiles the new README examples and runs them against the recording HTTP client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.getHtml returns the HTML preview of an email and its etag. The HTML is not ready to send. The API can return a temporary 503 error with a body that is not JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.getBlock returns one block of an email with its placement, and the etag of the email. The method checks the email ID and the block ID before it sends the request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.createBlock inserts a block into an email and returns the new block with its placement and the new etag. Add EmailBlockInput, EmailBlockPosition, EmailBlockTypes and EmailBlockPositions. The API uses placement.block_id only for the positions "before" and "after". So the SDK checks the block ID only for these positions. For "start" and "end", the SDK does not check the block ID and does not send it. The SDK does not change the placement of the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.updateBlock sends PATCH with the new editor content and the etag, and returns the block and the new etag. The new editor content replaces the whole content of the block. A block that the SDK read is sent back with the same whole numbers and the same null values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.deleteBlock sends DELETE with the etag in the body, and returns the new etag of the email. The API also deletes the blocks in the block. The old delete methods of the SDK still send no body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.moveBlock sends the new placement and the etag, and returns the block with its new placement and the new etag. The placement checks are the same as for an insert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.validateBlock checks a block with the rules of an insert and saves nothing. It sends no etag. An invalid block is not an error: the response has valid set to false and the errors for each field. Add EmailValidation, EmailSchema and EmailRejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.updateSettings sends PATCH with only the settings fields that the caller set, and the etag at the top level of the body. A null field is not sent. An empty string unsets a value, and an empty list of social links removes all the links. The response has the settings, the new etag and the unsafe values that the API removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.validateSettings checks a change to the design settings with the rules of an update and saves nothing. It sends no etag. The response has valid, the errors for each field and the unsafe values that an update would remove. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.createTemplate copies an email to a new template with the given name, and returns the template. An empty or null name throws IllegalArgumentException, and the SDK sends no request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.versions returns a retriever with page() and limit(). Its get() returns the versions of an email, newest first. Without options, the request has no query string. The pagination meta can have null for from and to, for example for an empty page. The SDK reads null as 0. The Javadoc of ResponseMeta now says this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.createVersion saves the email as a new version. The request has an empty body, not the JSON null. SingleEmailVersion.isCreated() is true for status 201 (a new version) and false for status 200 (nothing changed, and the response has the last version). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emails.restoreVersion puts an email back to a version. It sends the etag in the body, and returns the full email with the new etag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the BrandStyles service and the accessor MailerLite.brandStyles(). BrandStyles.get returns the brand styles of the account, the default palette and the presets. The brand is null when the account has none. The JSON key "default" is a Java keyword, so the field is defaultStyle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the MediaFiles service and the accessor MailerLite.files(). Its retriever lists the files of the media library, with a name filter, a type filter, any other filter, a sort, a page and a limit. Add MediaFileTypes and MediaFileSorts. The names File and Files are not used, because they clash with java.io.File and java.nio.file.Files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the fields editor, templateId and replyTo to EmailBase, and the constant EmailEditors.SIMPLE. The SDK does not send a field that is null, so a create without these fields sends the same body as before. The ID of the template is a String, like the other IDs of the SDK. Only the campaign create accepts editor and templateId. The Javadoc of the fields and of CampaignBuilder.update() says that an update with one of them gets a 422 error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RequestOptions.idempotencyKey(key). It checks the key at once. Add overloads with RequestOptions as the last argument to createBlock, updateBlock, deleteBlock, moveBlock, updateSettings, createTemplate, createVersion and restoreVersion. With a key, the request has the Idempotency-Key header. Without options or with a null key, the request has no header. The validate methods never send it. Add MailerLiteResponse.isIdempotentReplay(). It finds the header Idempotent-Replayed without case and compares its value with "true" without case. Add a README section that shows a retry after a network error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A network error such as a ConnectException or an InterruptedException can have a null message. Then the MailerLiteException also had a null message. Now the message is the class name and the message of the network error, for example "java.net.ConnectException". The cause and the interrupt flag do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK encodes each query value. A value that the caller encoded before is encoded again, so "summer%20sale" is sent as "summer%2520sale". A plus sign is sent as "%2B", so the API reads a literal plus sign. The tests keep this behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the README section "Upgrading to 0.1.0". It says that the SDK now encodes query values, so callers must not encode them, and that a plus sign is sent as a literal plus sign. It says that EmailBase now has reply_to, editor and template_id, so subclasses must remove their own fields with these JSON names. It also says that a network error is now a MailerLiteException with the network error as the cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs said that an empty string unsets any settings value. The API accepts an empty string only for a brand color. To unset a footer text, send an empty editor document. For the other values, for example other colors, sizes, button.borderRadius and brand.style, the API rejects an empty string with a 422 error. The Javadoc of the settings classes and of Emails.updateSettings, and the README, now say this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[High risk] The description says to merge and release only after the 16 endpoints appear in the public API documentation, and neither the description nor the review's account shows that this was checked; the review read no sources outside the repository. The risk stays high, as in the last review: the change is additive, but it changes existing behavior (query values are now encoded, FindingsEarlier findings (1 resolved, 1 dismissed)
SummaryAdds simple-editor email, brand style and file endpoints to the SDK The change adds What's good
Review context · 0/5 commands
Reviews (5) · Last reviewed commit: "fix: set the message field of MailerLite..." · claude-sonnet-5-5 |
Some tests compared the full URL of a request with more than one query parameter. The sequence of the parameters comes from the HashMap in ApiRequest, and the API gives no meaning to it. Add UrlAssert. It compares the path exactly, and the raw query pairs as a list in any sequence, with duplicates. The encoding is still checked exactly. ApiRequest does not change, so the recorded tapes still match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the doc comments that only repeat the code in private members, test helpers and tests. Keep the public Javadoc, and shorten the comments that explain a reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
| import java.util.HashMap; | ||
|
|
||
| import com.mailerlite.sdk.emails.EmailErrorCodes; |
There was a problem hiding this comment.
Core exception package now depends on the emails package · maintainability
MailerLiteException in exceptions imports com.mailerlite.sdk.emails.EmailErrorCodes only to implement isVersionConflict(). emails already depends on exceptions (Emails imports MailerLiteException), so this creates a package cycle. The cycle ties a generic exception to one service's constants.
Suggested fix
Keep the cycle out by comparing against a constant that lives in exceptions (for example a public static final String VERSION_CONFLICT on MailerLiteException). Make EmailErrorCodes.VERSION_CONFLICT alias it, or the reverse, and drop the emails import here.
Prompt for a coding agent
In MailerLiteException.java remove the import of com.mailerlite.sdk.emails.EmailErrorCodes. Add a constant `public static final String VERSION_CONFLICT_CODE = "version_conflict";` to MailerLiteException and make isVersionConflict() compare against it. Change EmailErrorCodes.VERSION_CONFLICT to reference MailerLiteException.VERSION_CONFLICT_CODE so the value stays defined in one place. Update the Javadoc {@link} on isVersionConflict accordingly.
The SDK now checks only the IDs in the URL path before a request. It sends the etag, the placement, the idempotency key and the block and template values as the caller gives them, and the API returns the errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Java HTTP client sends each character that is not ASCII as "?", so the API got a different key and ran the write. The SDK now rejects a key with a character outside 0x20 to 0x7E and sends no request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MailerLiteException now has the VERSION_CONFLICT constant, and EmailErrorCodes.VERSION_CONFLICT refers to it. The exceptions package no longer imports the emails package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public message field was always empty. It now has the message of the API error without the error code, and for other errors the text of getMessage(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why?
The MailerLite API adds endpoints to read and edit emails that are built with the simple editor. It also adds two related endpoints: brand styles and files. This PR adds all 16 endpoints to the Java SDK, so that Java users can build and edit these emails without raw HTTP requests.
Merge and release this only after the endpoints are in the public API documentation.
Changes
New
Emailsservice (com.mailerlite.sdk.emails), fromml.emails():getGET /emails/{email}getHtmlGET /emails/{email}/htmlgetBlockGET /emails/{email}/blocks/{block}createBlockPOST /emails/{email}/blocksupdateBlockPATCH /emails/{email}/blocks/{block}deleteBlockDELETE /emails/{email}/blocks/{block}moveBlockPOST /emails/{email}/blocks/{block}/movevalidateBlockPOST /emails/blocks/validateupdateSettingsPATCH /emails/{email}/settingsvalidateSettingsPOST /emails/{email}/settings/validatecreateTemplatePOST /emails/{email}/templatesversions(...).get()GET /emails/{email}/versionscreateVersionPOST /emails/{email}/versionsrestoreVersionPOST /emails/{email}/versions/{version}/restoreEmail,EmailBase), so the email model isEmailDocument.EmailBlockInput,EmailBlockPosition,EmailSettings).editor_contentis aMap<String, Object>with its own Gson adapter. Whole numbers stay whole (linkId: 7is not sent back as7.0), andnullvalues in the map are kept.nullfield is not sent. Only a brand colour accepts an empty string, and an empty editor document unsets a footer text.New
BrandStylesservice (com.mailerlite.sdk.brandstyles):ml.brandStyles().get()callsGET /brand-styles.brandisnullwhen the account has no brand styles.New
MediaFilesservice (com.mailerlite.sdk.files):ml.files().retriever()callsGET /files, withnameFilter,typeFilter,sort,pageandlimit. The names do not useFileorFiles, so that they do not clash withjava.io.Fileandjava.nio.file.Files.HTTP layer (
MailerLiteApi)sendRequestmethod supports PATCH, a DELETE body (onlydeleteBlocksends one) and a POST with no body (createVersion). The old request methods do not change.MailerLiteExceptionhas new fields:errorCodeandetag, a new method,isVersionConflict(), and a new constant,VERSION_CONFLICT.codestays the HTTP status. When the API sends a code,getMessage()starts with it.EmailErrorCodes.VERSION_CONFLICTrefers to the constant, so theexceptionspackage does not import theemailspackage.MailerLiteException.messagenow has the message of the API error, without the code. Before, it was always""(also onmain). If the SDK cannot read the error body, or for a network error, it has the same text asgetMessage().MailerLiteExceptionwith the network error as the cause. Before, all request methods threw aClassCastException.ApiRequestnow encodes the query values and skipsnullvalues. This also applies to the old retrievers (see "Upgrading to 0.1.0").Checks before the request: the SDK checks only the IDs in the URL path and the characters of the idempotency key. If a check fails, the SDK throws the error and sends no request.
EmptyIdExceptionorInvalidIdException. Both are unchecked subclasses ofIllegalArgumentException. Reason: the API cannot see this problem. It decodes an escaped/in a path, so an escaped ID could reach a different endpoint.block_id, the block type,editor_contentand the template name. The SDK does not repeat the API rules, so a rule change in the API needs no SDK release.nullblock increateBlockandnullsettings inupdateSettingsthrowNullPointerException. InvalidateBlockandvalidateSettings, anullargument sends an empty body, and the API returns the validation errors.Idempotency keys: the 8 email writes have an overload with a last
RequestOptionsargument:RequestOptions.idempotencyKey(key). The SDK sends it as theIdempotency-Keyheader. If a request with the same key already succeeded, the API returns the stored response again and does not do the write two times. This makes it safe to retry a write after a network error. The SDK sends the key as given, and the API checks it (an invalid key gets400). Only one exception: a key with a character outside printable ASCII (0x20to0x7E) throwsInvalidIdempotencyKeyException(anIllegalArgumentException), and the SDK sends no request. Reason: the Java HTTP client sends each character that is not ASCII as?, sok-é-kreached a development instance of the API as the valid keyk-?-k, and the write ran. Anullor empty key sends no header.MailerLiteResponse.isIdempotentReplay()is true when the API returned a stored response.Campaigns:
EmailBasehas three new fields:replyTo,editorandtemplateId. SeteditortoEmailEditors.SIMPLEto create an email with the simple editor, and then edit it withml.emails(). Only create acceptseditorandtemplateId. An update with these fields gets a422.Constants: block types, block positions, brand modes, the simple editor, the
version_conflicterror code, file types and file sorts. They areStringconstants, so a new value from the API does not break the decode.README: a new "Upgrading to 0.1.0" section, new "Emails", "Brand styles" and "Files" sections with one example for each method, a "Retry a write with an idempotency key" example, and "Create a campaign with the simple editor".
Version:
0.1.0inpom.xmland in the README install snippets.Compatibility
All changes add to the public API. A
javapcomparison ofmainand this branch shows no public or protected member that was removed or changed. Some behavior changes, and the README section "Upgrading to 0.1.0" describes each one:+in a query value is now sent as a literal+.EmailBasewith its ownreply_to,editorortemplate_idfield now makes Gson throw an error.MailerLiteException, not aClassCastException.MailerLiteException.messagenow has a value. Before, it was always"".getMessage()does not change.Tests
HttpClientthat keeps each request and its raw body, and returns queued responses. They use no recorded tapes and no account, and they cannot reach the API.409withversion_conflict,422witherrors, and503with an HTML body;createVersionwith201and with200, and with an empty raw body;brand: null, a file name filter with spaces, and the encoding of an encoded value;block_idfor each position, an empty block type and an empty template name: the SDK sends each one as given, and the test checks the exact body and the API error. Anulletag sends noetagkey;true,True, no header), and keys with spaces,~or more than 255 characters. The SDK gives each key to the Java HTTP client as given. The client removes the spaces at the start and the end;é, a CJK character, an emoji, a new line, a tab,DELor another control character: the SDK throwsInvalidIdempotencyKeyExceptionand sends no request;messagefield ofMailerLiteExceptionfor each request method, for an error with a code, for an error body without a message, for an HTML body and for a network error;exceptionspackage does not import theemailspackage;editorandtemplate_idare present, andcontentis absent.mvn testpasses on JDK 11 and on JDK 21 (177 tests, 0 failures, 0 errors). All old tests still match their recorded tapes.mvn javadoc:javadocon JDK 11 gives no warnings.HttpClient.409(old etag),404(unknown block) and422(invalid file type), and an idempotency check: a retry with the same key returned the stored201with the same block ID, the same key with a different body returned422, and a retry without a key returned409. These runs were before the change that leaves the value checks to the API.How to test
mvn test.pom.xmlsetstestFailureIgnore, so read theTests runsummary, not the exit code.etagof each response to the next write.MailerLiteExceptionwhereisVersionConflict()is true.isIdempotentReplay()is true.How to release
v0.1.0. The publish workflow checks that the tag is the same as the version inpom.xml, and then publishes to Maven Central. This is a minor version because of the behavior changes in "Upgrading to 0.1.0".Rollback
No data or configuration changes. To roll back, revert the merge commit and release a new patch version.
🤖 Generated with Claude Code