-
Notifications
You must be signed in to change notification settings - Fork 5
Contacts API #49
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
base: main
Are you sure you want to change the base?
Contacts API #49
Changes from 4 commits
8b2cac6
4471f3f
1f7c59b
01e8b32
159c35b
e99c504
b30b772
c76a3e9
bc311cf
46fcb82
b8bc602
f56bc2b
c04ee3c
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,44 @@ | ||
require 'mailtrap' | ||
|
||
client = Mailtrap::Client.new(api_key: 'your-api-key') | ||
contact_list = Mailtrap::ContactListsAPI.new 3229, client | ||
contacts = Mailtrap::ContactsAPI.new 3229, client | ||
|
||
# Set your API credentials as environment variables | ||
# export MAILTRAP_API_KEY='your-api-key' | ||
# export MAILTRAP_ACCOUNT_ID=your-account-id | ||
# | ||
# contact_list = Mailtrap::ContactListsAPI.new | ||
# contacts = Mailtrap::ContactsAPI.new | ||
|
||
# Create new contact list | ||
list = contact_list.create(name: 'Test List') | ||
|
||
# Get all contact lists | ||
contact_list.list | ||
|
||
# Update contact list | ||
contact_list.update(list.id, name: 'Test List Updated') | ||
|
||
# Get contact list | ||
list = contact_list.get(list.id) | ||
|
||
# Create new contact | ||
contact = contacts.create(email: '[email protected]', fields: { first_name: 'John Doe' }, list_ids: [list.id]) | ||
|
||
# Get contact | ||
contact = contacts.get(contact.id) | ||
|
||
# Update contact using id | ||
updated_contact = contacts.update(contact.id, email: '[email protected]', fields: { first_name: 'Jane Doe' }, | ||
list_ids_excluded: [list.id]) | ||
|
||
# Update contact using email | ||
contacts.update(updated_contact.data.email, email: '[email protected]', fields: { first_name: 'Jane Doe' }, | ||
list_ids_included: [list.id]) | ||
|
||
# Delete contact | ||
contacts.delete(contact.id) | ||
|
||
# Delete contact list | ||
contact_list.delete(list.id) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,11 @@ | |
require_relative 'mailtrap/action_mailer' if defined? ActionMailer | ||
require_relative 'mailtrap/mail' | ||
require_relative 'mailtrap/errors' | ||
require_relative 'mailtrap/base_api' | ||
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 dont need to require it here. Please require the base api only where it is needed, in api implementations. |
||
require_relative 'mailtrap/version' | ||
require_relative 'mailtrap/email_templates_api' | ||
require_relative 'mailtrap/contacts_api' | ||
require_relative 'mailtrap/contact_lists_api' | ||
|
||
module Mailtrap | ||
# @!macro api_errors | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# frozen_string_literal: true | ||
|
||
module Mailtrap | ||
class BaseAPI | ||
Contributor
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. This class has a bunch of helper methods. I wonder if it should be a mixin instead. def delete(id)
client.delete("#{base_path}/#{id}")
end See also the abstract yard annotation tag. 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 wouldn't go with mixin, cause main goal was to share 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. also I can advice to have base implementations for all methods here def create(options)
validate_options!(options, supported_options)
response = client.post(base_path, options)
build_entity(response, response_entity)
end
private
def response_entity
raise NotImplementedError
end
def supported_options
raise NotImplementedError
end and it will be implemented like class ContactsApi < BaseApi
...
def supported_options
SUPPORTED_OPTIONS
end
...
end
Contributor
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. Yeah that what I meant. Though keep in mind that ruby is not java, thus you don't need to implement methods with |
||
attr_reader :account_id, :client | ||
|
||
# @param account_id [Integer] The account ID | ||
# @param client [Mailtrap::Client] The client instance | ||
# @raise [ArgumentError] If account_id is nil | ||
def initialize(account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new) | ||
raise ArgumentError, 'account_id is required' if account_id.nil? | ||
Comment on lines
+29
to
+33
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. 💡 Verification agent 🧩 Analysis chainVerify account_id parameter type consistency The comment indicates 🏁 Script executed: #!/bin/bash
# Description: Check how account_id is used across the codebase
# Check if account_id is used in string interpolation (which would work with both types)
rg -A 2 'account_id' --type ruby
# Check if there are any type conversions for account_id
ast-grep --pattern 'account_id.to_$_' Length of output: 1980 Ensure consistent Currently,
Pick one approach and update 🤖 Prompt for AI Agents
|
||
|
||
@account_id = account_id | ||
@client = client | ||
end | ||
|
||
private | ||
|
||
def validate_options!(options, supported_options) | ||
invalid_options = options.keys - supported_options | ||
return if invalid_options.empty? | ||
|
||
raise ArgumentError, "invalid options are given: #{invalid_options}, supported_options: #{supported_options}" | ||
end | ||
|
||
def build_entity(options, response_class) | ||
response_class.new(options.slice(*response_class.members)) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# frozen_string_literal: true | ||
|
||
module Mailtrap | ||
# Data Transfer Object for Contact | ||
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/220a54e31e5ca-contact | ||
# @attr_reader id [String] The contact ID | ||
# @attr_reader email [String] The contact's email address | ||
# @attr_reader fields [Hash] Object of fields with merge tags | ||
# @attr_reader list_ids [Array<Integer>] Array of list IDs | ||
# @attr_reader status [String] The contact status (subscribed/unsubscribed) | ||
# @attr_reader created_at [Integer] The creation timestamp | ||
# @attr_reader updated_at [Integer] The last update timestamp | ||
Contact = Struct.new( | ||
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. @IgorDobryn if the only motivation for resource classes it to achieve strictness, then we can do this: contact_hash = {id: 1}
contact_hash.default_proc = proc { |_, k| raise "unknown key '#{k}'" } Thoughts? 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. It can work, but rather unexpected. So, I'd prefer to keep separate class |
||
:id, | ||
:email, | ||
:fields, | ||
:list_ids, | ||
:status, | ||
:created_at, | ||
:updated_at, | ||
keyword_init: true | ||
) do | ||
# @return [Hash] The contact attributes as a hash | ||
def to_h | ||
super.compact | ||
end | ||
end | ||
|
||
# Data Transfer Object for Contact Update Response | ||
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/16eab4fff9740-contact-update-response | ||
# @attr_reader action [String] The performed action (created/updated) | ||
# @attr_reader data [Contact, Hash] The contact data | ||
ContactUpdateResponse = Struct.new(:action, :data, keyword_init: true) do | ||
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. In my opinion it creates inconsistency. How about adding |
||
def initialize(*) | ||
super | ||
self.data = Contact.new(data) if data.is_a?(Hash) | ||
end | ||
|
||
# @return [Hash] The response attributes as a hash | ||
def to_h | ||
super.compact | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# frozen_string_literal: true | ||
|
||
module Mailtrap | ||
# Data Transfer Object for Contact List | ||
# @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/6ec7a37234af2-contact-list | ||
# @attr_reader id [Integer] The contact list ID | ||
# @attr_reader name [String] The name of the contact list | ||
ContactList = Struct.new(:id, :name, keyword_init: true) do | ||
# @return [Hash] The contact list attributes as a hash | ||
def to_h | ||
super.compact | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'contact_list' | ||
|
||
module Mailtrap | ||
class ContactListsAPI < BaseAPI | ||
Contributor
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. do you plan to add Contact Fields API support? |
||
SUPPORTED_OPTIONS = %i[name].freeze | ||
private_constant :SUPPORTED_OPTIONS | ||
|
||
# Retrieves a specific contact list | ||
# @param list_id [Integer] The contact list identifier | ||
# @return [ContactList] Contact list object | ||
# @!macro api_errors | ||
def get(list_id) | ||
response = client.get("#{base_path}/#{list_id}") | ||
build_entity(response, ContactList) | ||
end | ||
|
||
# Creates a new contact list | ||
# @param [Hash] options The parameters to create | ||
# @option options [String] :name The contact list name | ||
# @return [ContactList] Created contact list object | ||
# @!macro api_errors | ||
# @raise [ArgumentError] If invalid options are provided | ||
def create(options) | ||
validate_options!(options, SUPPORTED_OPTIONS) | ||
|
||
response = client.post(base_path, options) | ||
build_entity(response, ContactList) | ||
end | ||
|
||
# Updates an existing contact list | ||
# @param list_id [Integer] The contact list ID | ||
# @param [Hash] options The parameters to update | ||
# @option options [String] :name The contact list name | ||
# @return [ContactList] Updated contact list object | ||
# @!macro api_errors | ||
# @raise [ArgumentError] If invalid options are provided | ||
def update(list_id, options) | ||
validate_options!(options, SUPPORTED_OPTIONS) | ||
|
||
response = client.patch( | ||
"#{base_path}/#{list_id}", options | ||
) | ||
build_entity(response, ContactList) | ||
end | ||
|
||
# Deletes a contact list | ||
# @param list_id [Integer] The contact list ID | ||
# @return nil | ||
# @!macro api_errors | ||
def delete(list_id) | ||
client.delete("#{base_path}/#{list_id}") | ||
end | ||
|
||
# Lists all contact lists for the account | ||
# @return [Array<ContactList>] Array of contact list objects | ||
# @!macro api_errors | ||
def list | ||
response = client.get(base_path) | ||
response.map { |list| build_entity(list, ContactList) } | ||
end | ||
|
||
private | ||
|
||
def base_path | ||
"/api/accounts/#{account_id}/contacts/lists" | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'contact' | ||
require 'ostruct' | ||
|
||
module Mailtrap | ||
class ContactsAPI < BaseAPI | ||
CREATE_SUPPORTED_OPTIONS = %i[email fields list_ids].freeze | ||
UPDATE_SUPPORTED_OPTIONS = %i[email fields list_ids_included list_ids_excluded unsubscribed].freeze | ||
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 believe
|
||
private_constant :CREATE_SUPPORTED_OPTIONS, :UPDATE_SUPPORTED_OPTIONS | ||
|
||
# Retrieves a specific contact | ||
# @param contact_id [String] The contact identifier, which can be either a UUID or an email address | ||
# @return [Contact] Contact object | ||
# @!macro api_errors | ||
def get(contact_id) | ||
response = client.get("#{base_path}/#{contact_id}") | ||
build_entity(response[:data], Contact) | ||
end | ||
|
||
# Creates a new contact | ||
# @param [Hash] options The parameters to create | ||
# @option options [String] :email The contact's email address | ||
# @option options [Hash] :fields The contact's fields | ||
# @option options [Array<Integer>] :list_ids The contact's list IDs | ||
# @return [Contact] Created contact object | ||
# @!macro api_errors | ||
# @raise [ArgumentError] If invalid options are provided | ||
def create(options) | ||
validate_options!(options, CREATE_SUPPORTED_OPTIONS) | ||
|
||
response = client.post(base_path, { contact: options }) | ||
build_entity(response[:data], Contact) | ||
end | ||
|
||
# Updates an existing contact | ||
# @param contact_id [String] The contact ID or email address | ||
# @param [Hash] options The parameters to update | ||
# @option options [String] :email The contact's email address | ||
# @option options [Hash] :fields The contact's fields | ||
# @option options [Array<Integer>] :list_ids_included The contact's list IDs to include | ||
# @option options [Array<Integer>] :list_ids_excluded The contact's list IDs to exclude | ||
# @option options [Boolean] :unsubscribed Whether to unsubscribe the contact | ||
# @return [ContactUpdateResponse] Updated contact object | ||
# @!macro api_errors | ||
# @raise [ArgumentError] If invalid options are provided | ||
def update(contact_id, options) | ||
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. How about renaming it to upsert to highlight what it really does? |
||
validate_options!(options, UPDATE_SUPPORTED_OPTIONS) | ||
|
||
response = client.patch( | ||
"#{base_path}/#{contact_id}", | ||
{ contact: options } | ||
) | ||
build_entity(response, ContactUpdateResponse) | ||
end | ||
|
||
# Deletes a contact | ||
# @param contact_id [String] The contact ID | ||
# @return nil | ||
# @!macro api_errors | ||
def delete(contact_id) | ||
client.delete("#{base_path}/#{contact_id}") | ||
end | ||
|
||
private | ||
|
||
def base_path | ||
"/api/accounts/#{account_id}/contacts" | ||
end | ||
end | ||
end |
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.
rename the variable. use plural form for consistency.