nylas 4.0.0.rc2 → 4.0.0.rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/nylas.rb +48 -5
- data/lib/nylas/account.rb +31 -0
- data/lib/nylas/api.rb +86 -2
- data/lib/nylas/calendar.rb +27 -0
- data/lib/nylas/collection.rb +46 -19
- data/lib/nylas/constraints.rb +12 -4
- data/lib/nylas/contact.rb +10 -2
- data/lib/nylas/current_account.rb +1 -4
- data/lib/nylas/delta.rb +46 -0
- data/lib/nylas/deltas.rb +17 -0
- data/lib/nylas/deltas_collection.rb +36 -0
- data/lib/nylas/draft.rb +51 -0
- data/lib/nylas/email_address.rb +1 -5
- data/lib/nylas/errors.rb +13 -0
- data/lib/nylas/event.rb +42 -0
- data/lib/nylas/event_collection.rb +13 -0
- data/lib/nylas/file.rb +58 -0
- data/lib/nylas/folder.rb +22 -0
- data/lib/nylas/http_client.rb +38 -29
- data/lib/nylas/im_address.rb +0 -5
- data/lib/nylas/label.rb +22 -0
- data/lib/nylas/message.rb +45 -0
- data/lib/nylas/message_headers.rb +25 -0
- data/lib/nylas/message_tracking.rb +11 -0
- data/lib/nylas/model.rb +75 -24
- data/lib/nylas/model/attributable.rb +2 -2
- data/lib/nylas/model/attributes.rb +3 -1
- data/lib/nylas/native_authentication.rb +31 -0
- data/lib/nylas/new_message.rb +29 -0
- data/lib/nylas/nylas_date.rb +5 -2
- data/lib/nylas/participant.rb +10 -0
- data/lib/nylas/phone_number.rb +0 -5
- data/lib/nylas/physical_address.rb +0 -5
- data/lib/nylas/raw_message.rb +15 -0
- data/lib/nylas/recurrence.rb +9 -0
- data/lib/nylas/rsvp.rb +18 -0
- data/lib/nylas/search_collection.rb +8 -0
- data/lib/nylas/thread.rb +52 -0
- data/lib/nylas/timespan.rb +18 -0
- data/lib/nylas/types.rb +62 -9
- data/lib/nylas/version.rb +1 -1
- data/lib/nylas/web_page.rb +0 -5
- data/lib/nylas/webhook.rb +19 -0
- metadata +31 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fac446793677a8c9ae9f69d5d70e1ef43cb5a452e600f590665afa2f6d82f0de
|
4
|
+
data.tar.gz: d42bf8cf84bf85be8e4a3b6bf896492da0e3e6795f6eb5afb538d6cdc55cad78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fe7bdd1b56c7fb1e37ce318c28ed419bf38124e052c8a5a5de61c609d1132cdeff7a728126d549e3e0da0388303585e63701fdc37f01cab6b0c773d7587729c
|
7
|
+
data.tar.gz: 33d014b9b2cbc74fc1d52a5b203017d9272952baa207ada25e89aef029051539f4d1f9066b72a055e319783b13dee6bb4a5f0130c69f9de1a675de0b1bc0e11a
|
data/lib/nylas.rb
CHANGED
@@ -17,15 +17,42 @@ require_relative "nylas/model"
|
|
17
17
|
|
18
18
|
# Attribute types supported by the API
|
19
19
|
require_relative "nylas/email_address"
|
20
|
+
require_relative "nylas/event"
|
21
|
+
require_relative "nylas/event_collection"
|
22
|
+
require_relative "nylas/file"
|
23
|
+
require_relative "nylas/folder"
|
20
24
|
require_relative "nylas/im_address"
|
25
|
+
require_relative "nylas/label"
|
26
|
+
require_relative "nylas/message_headers"
|
27
|
+
require_relative "nylas/message_tracking"
|
28
|
+
require_relative "nylas/participant"
|
21
29
|
require_relative "nylas/physical_address"
|
22
30
|
require_relative "nylas/phone_number"
|
31
|
+
require_relative "nylas/recurrence"
|
32
|
+
require_relative "nylas/rsvp"
|
33
|
+
require_relative "nylas/timespan"
|
23
34
|
require_relative "nylas/web_page"
|
24
35
|
require_relative "nylas/nylas_date"
|
25
36
|
|
37
|
+
# Custom collection types
|
38
|
+
require_relative "nylas/search_collection"
|
39
|
+
require_relative "nylas/deltas_collection"
|
40
|
+
|
26
41
|
# Models supported by the API
|
42
|
+
require_relative "nylas/account"
|
43
|
+
require_relative "nylas/calendar"
|
27
44
|
require_relative "nylas/contact"
|
28
45
|
require_relative "nylas/current_account"
|
46
|
+
require_relative "nylas/deltas"
|
47
|
+
require_relative "nylas/delta"
|
48
|
+
require_relative "nylas/draft"
|
49
|
+
require_relative "nylas/message"
|
50
|
+
require_relative "nylas/new_message"
|
51
|
+
require_relative "nylas/raw_message"
|
52
|
+
require_relative "nylas/thread"
|
53
|
+
require_relative "nylas/webhook"
|
54
|
+
|
55
|
+
require_relative "nylas/native_authentication"
|
29
56
|
|
30
57
|
require_relative "nylas/http_client"
|
31
58
|
require_relative "nylas/api"
|
@@ -33,10 +60,26 @@ require_relative "nylas/api"
|
|
33
60
|
# an SDK for interacting with the Nylas API
|
34
61
|
# @see https://docs.nylas.com/reference
|
35
62
|
module Nylas
|
36
|
-
Types.registry[:
|
37
|
-
Types.registry[:
|
38
|
-
Types.registry[:
|
39
|
-
Types.registry[:
|
40
|
-
Types.registry[:
|
63
|
+
Types.registry[:account] = Types::ModelType.new(model: Account)
|
64
|
+
Types.registry[:calendar] = Types::ModelType.new(model: Calendar)
|
65
|
+
Types.registry[:contact] = Types::ModelType.new(model: Contact)
|
66
|
+
Types.registry[:delta] = DeltaType.new
|
67
|
+
Types.registry[:draft] = Types::ModelType.new(model: Draft)
|
68
|
+
Types.registry[:email_address] = Types::ModelType.new(model: EmailAddress)
|
69
|
+
Types.registry[:event] = Types::ModelType.new(model: Event)
|
70
|
+
Types.registry[:file] = Types::ModelType.new(model: File)
|
71
|
+
Types.registry[:folder] = Types::ModelType.new(model: Folder)
|
72
|
+
Types.registry[:im_address] = Types::ModelType.new(model: IMAddress)
|
73
|
+
Types.registry[:label] = Types::ModelType.new(model: Label)
|
74
|
+
Types.registry[:message] = Types::ModelType.new(model: Message)
|
75
|
+
Types.registry[:message_headers] = MessageHeadersType.new
|
76
|
+
Types.registry[:message_tracking] = Types::ModelType.new(model: MessageTracking)
|
77
|
+
Types.registry[:participant] = Types::ModelType.new(model: Participant)
|
78
|
+
Types.registry[:physical_address] = Types::ModelType.new(model: PhysicalAddress)
|
79
|
+
Types.registry[:phone_number] = Types::ModelType.new(model: PhoneNumber)
|
80
|
+
Types.registry[:recurrence] = Types::ModelType.new(model: Recurrence)
|
81
|
+
Types.registry[:thread] = Types::ModelType.new(model: Thread)
|
82
|
+
Types.registry[:timespan] = Types::ModelType.new(model: Timespan)
|
83
|
+
Types.registry[:web_page] = Types::ModelType.new(model: WebPage)
|
41
84
|
Types.registry[:nylas_date] = NylasDateType.new
|
42
85
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Representation of the accounts for Account management purposes.
|
3
|
+
# @see https://docs.nylas.com/reference#account-management
|
4
|
+
class Account
|
5
|
+
include Model
|
6
|
+
self.listable = true
|
7
|
+
self.showable = true
|
8
|
+
|
9
|
+
attribute :id, :string
|
10
|
+
attribute :account_id, :string
|
11
|
+
attribute :billing_state, :string
|
12
|
+
attribute :sync_state, :string
|
13
|
+
|
14
|
+
attribute :email, :string
|
15
|
+
attribute :trial, :boolean
|
16
|
+
|
17
|
+
def upgrade
|
18
|
+
response = execute(method: :post, path: "#{resource_path}/upgrade")
|
19
|
+
response[:success]
|
20
|
+
end
|
21
|
+
|
22
|
+
def downgrade
|
23
|
+
response = execute(method: :post, path: "#{resource_path}/downgrade")
|
24
|
+
response[:success]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.resources_path(api:)
|
28
|
+
"/a/#{api.app_id}/accounts"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/nylas/api.rb
CHANGED
@@ -3,7 +3,7 @@ module Nylas
|
|
3
3
|
class API
|
4
4
|
attr_accessor :client
|
5
5
|
extend Forwardable
|
6
|
-
def_delegators :client, :execute, :get, :post, :put, :delete
|
6
|
+
def_delegators :client, :execute, :get, :post, :put, :delete, :app_id
|
7
7
|
|
8
8
|
include Logging
|
9
9
|
|
@@ -24,6 +24,13 @@ module Nylas
|
|
24
24
|
end
|
25
25
|
# rubocop:enable Metrics/ParameterLists
|
26
26
|
|
27
|
+
# @return [String] A Nylas access token for that particular user.
|
28
|
+
def authenticate(name:, email_address:, provider:, settings:, reauth_account_id: nil)
|
29
|
+
NativeAuthentication.new(api: self).authenticate(name: name, email_address: email_address,
|
30
|
+
provider: provider, settings: settings,
|
31
|
+
reauth_account_id: reauth_account_id)
|
32
|
+
end
|
33
|
+
|
27
34
|
# @return [Collection<Contact>] A queryable collection of Contacts
|
28
35
|
def contacts
|
29
36
|
@contacts ||= Collection.new(model: Contact, api: self)
|
@@ -32,7 +39,84 @@ module Nylas
|
|
32
39
|
# @return [CurrentAccount] The account details for whomevers access token is set
|
33
40
|
def current_account
|
34
41
|
prevent_calling_if_missing_access_token(:current_account)
|
35
|
-
CurrentAccount.from_hash(
|
42
|
+
CurrentAccount.from_hash(execute(method: :get, path: "/account"), api: self)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Collection<Account>] A queryable collection of {Account}s
|
46
|
+
def accounts
|
47
|
+
@accounts ||= Collection.new(model: Account, api: as(client.app_secret))
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Collection<Calendar>] A queryable collection of {Calendar}s
|
51
|
+
def calendars
|
52
|
+
@calendars ||= Collection.new(model: Calendar, api: self)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [DeltasCollection<Delta>] A queryable collection of Deltas, which are themselves a collection.
|
56
|
+
def deltas
|
57
|
+
@deltas ||= DeltasCollection.new(api: self)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return[Collection<Draft>] A queryable collection of {Draft} objects
|
61
|
+
def drafts
|
62
|
+
@drafts ||= Collection.new(model: Draft, api: self)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Collection<Event>] A queryable collection of {Event}s
|
66
|
+
def events
|
67
|
+
@events ||= EventCollection.new(model: Event, api: self)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Collection<Folder>] A queryable collection of {Folder}s
|
71
|
+
def folders
|
72
|
+
@folders ||= Collection.new(model: Folder, api: self)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Collection<File>] A queryable collection of {File}s
|
76
|
+
def files
|
77
|
+
@files ||= Collection.new(model: File, api: self)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Collection<Label>] A queryable collection of {Label} objects
|
81
|
+
def labels
|
82
|
+
@labels ||= Collection.new(model: Label, api: self)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return[Collection<Message>] A queryable collection of {Message} objects
|
86
|
+
def messages
|
87
|
+
@messages ||= Collection.new(model: Message, api: self)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Revokes access to the Nylas API for the given access token
|
91
|
+
# @return [Boolean]
|
92
|
+
def revoke(access_token)
|
93
|
+
response = client.as(access_token).post(path: "/oauth/revoke")
|
94
|
+
response.code == 200 && response.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param message [Hash, String, #send!]
|
98
|
+
# @return [Message] The resulting message
|
99
|
+
def send!(message)
|
100
|
+
return message.send! if message.respond_to?(:send!)
|
101
|
+
return NewMessage.new(**message.merge(api: self)).send! if message.respond_to?(:key?)
|
102
|
+
return RawMessage.new(message, api: self).send! if message.is_a? String
|
103
|
+
end
|
104
|
+
|
105
|
+
# Allows you to get an API that acts as a different user but otherwise has the same settings
|
106
|
+
# @param [String] Oauth Access token or app secret used to authenticate with the API
|
107
|
+
# @return [API]
|
108
|
+
def as(access_token)
|
109
|
+
API.new(client: client.as(access_token))
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Collection<Thread>] A queryable collection of Threads
|
113
|
+
def threads
|
114
|
+
@threads ||= Collection.new(model: Thread, api: self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Collection<Webhook>] A queryable collection of {Webhook}s
|
118
|
+
def webhooks
|
119
|
+
@webhooks ||= Collection.new(model: Webhook, api: as(client.app_secret))
|
36
120
|
end
|
37
121
|
|
38
122
|
private def prevent_calling_if_missing_access_token(method_name)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Ruby bindings for the Nylas Calendar API
|
3
|
+
# @see https://docs.nylas.com/reference#calendars
|
4
|
+
class Calendar
|
5
|
+
include Model
|
6
|
+
self.resources_path = "/calendars"
|
7
|
+
allows_operations(listable: true, filterable: true, showable: true)
|
8
|
+
|
9
|
+
attribute :id, :string
|
10
|
+
attribute :account_id, :string
|
11
|
+
|
12
|
+
attribute :object, :string
|
13
|
+
|
14
|
+
attribute :name, :string
|
15
|
+
attribute :description, :string
|
16
|
+
|
17
|
+
attribute :read_only, :boolean
|
18
|
+
|
19
|
+
def read_only?
|
20
|
+
read_only == true
|
21
|
+
end
|
22
|
+
|
23
|
+
def events
|
24
|
+
api.events.where(calendar_id: id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/nylas/collection.rb
CHANGED
@@ -2,56 +2,70 @@ module Nylas
|
|
2
2
|
# An enumerable for working with index and search endpoints
|
3
3
|
class Collection
|
4
4
|
attr_accessor :model, :api, :constraints
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :each, :map, :select, :reject, :to_a, :take
|
7
|
+
def_delegators :to_a, :first, :last, :[]
|
8
|
+
|
5
9
|
def initialize(model:, api:, constraints: nil)
|
6
10
|
self.constraints = Constraints.from_constraints(constraints)
|
7
11
|
self.model = model
|
8
12
|
self.api = api
|
9
13
|
end
|
10
14
|
|
15
|
+
# Instantiates a new model
|
11
16
|
def new(**attributes)
|
12
|
-
model.
|
17
|
+
model.new(attributes.merge(api: api))
|
13
18
|
end
|
14
19
|
|
15
20
|
def create(**attributes)
|
16
|
-
model.
|
17
|
-
instance = model.from_hash(attributes, api: api)
|
21
|
+
instance = model.new(attributes.merge(api: api))
|
18
22
|
instance.save
|
19
23
|
instance
|
20
24
|
end
|
21
25
|
|
26
|
+
# Merges in additional filters when querying the collection
|
27
|
+
# @return [Collection<Model>]
|
22
28
|
def where(filters)
|
23
|
-
raise
|
29
|
+
raise ModelNotFilterableError, model unless model.filterable?
|
24
30
|
self.class.new(model: model, api: api, constraints: constraints.merge(where: filters))
|
25
31
|
end
|
26
32
|
|
33
|
+
def search(query)
|
34
|
+
raise ModelNotSearchableError, model unless model.searchable?
|
35
|
+
SearchCollection.new(model: model, api: api, constraints: constraints.merge(where: { q: query }))
|
36
|
+
end
|
37
|
+
|
38
|
+
# The collection now returns a string representation of the model in a particular mime type instead of
|
39
|
+
# Model objects
|
40
|
+
# @return [Collection<String>]
|
41
|
+
def raw
|
42
|
+
raise ModelNotAvailableAsRawError, model unless model.exposable_as_raw?
|
43
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(accept: model.raw_mime_type))
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Integer]
|
27
47
|
def count
|
28
48
|
self.class.new(model: model, api: api, constraints: constraints.merge(view: "count")).execute[:count]
|
29
49
|
end
|
30
50
|
|
31
|
-
|
32
|
-
|
51
|
+
# @return [Collection<Model>]
|
52
|
+
def expanded
|
53
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(view: "expanded"))
|
33
54
|
end
|
34
55
|
|
35
|
-
|
36
|
-
|
56
|
+
# @return [Array<String>]
|
57
|
+
def ids
|
58
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(view: "ids")).execute
|
37
59
|
end
|
38
60
|
|
39
61
|
# Iterates over a single page of results based upon current pagination settings
|
40
62
|
def each
|
41
63
|
return enum_for(:each) unless block_given?
|
42
64
|
execute.each do |result|
|
43
|
-
yield(model.
|
65
|
+
yield(model.new(result.merge(api: api)))
|
44
66
|
end
|
45
67
|
end
|
46
68
|
|
47
|
-
def to_a
|
48
|
-
each.to_a
|
49
|
-
end
|
50
|
-
|
51
|
-
def map(&block)
|
52
|
-
each.map(&block)
|
53
|
-
end
|
54
|
-
|
55
69
|
def limit(quantity)
|
56
70
|
self.class.new(model: model, api: api, constraints: constraints.merge(limit: quantity))
|
57
71
|
end
|
@@ -76,7 +90,7 @@ module Nylas
|
|
76
90
|
end
|
77
91
|
end
|
78
92
|
|
79
|
-
def next_page(accumulated
|
93
|
+
def next_page(accumulated:, current_page:)
|
80
94
|
return nil unless more_pages?(accumulated, current_page)
|
81
95
|
self.class.new(model: model, api: api, constraints: constraints.next_page)
|
82
96
|
end
|
@@ -91,6 +105,18 @@ module Nylas
|
|
91
105
|
# Retrieves a record. Nylas doesn't support where filters on GET so this will not take into
|
92
106
|
# consideration other query constraints, such as where clauses.
|
93
107
|
def find(id)
|
108
|
+
constraints.accept == "application/json" ? find_model(id) : find_raw(id)
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_raw(id)
|
112
|
+
api.execute(to_be_executed.merge(path: "#{resources_path}/#{id}")).to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
def resources_path
|
116
|
+
model.resources_path(api: api)
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_model(id)
|
94
120
|
instance = model.from_hash({ id: id }, api: api)
|
95
121
|
instance.reload
|
96
122
|
instance
|
@@ -98,7 +124,8 @@ module Nylas
|
|
98
124
|
|
99
125
|
# @return [Hash] Specification for request to be passed to {API#execute}
|
100
126
|
def to_be_executed
|
101
|
-
{ method: :get, path:
|
127
|
+
{ method: :get, path: resources_path, query: constraints.to_query,
|
128
|
+
headers: constraints.to_headers }
|
102
129
|
end
|
103
130
|
|
104
131
|
# Retrieves the data from the API for the particular constraints
|
data/lib/nylas/constraints.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
1
|
module Nylas
|
2
2
|
# The constraints a particular GET request will include in their query params
|
3
3
|
class Constraints
|
4
|
-
attr_accessor :where, :limit, :offset, :view, :per_page
|
5
|
-
|
4
|
+
attr_accessor :where, :limit, :offset, :view, :per_page, :accept
|
5
|
+
# rubocop:disable Metrics/ParameterLists
|
6
|
+
def initialize(where: {}, limit: nil, offset: 0, view: nil, per_page: 100, accept: "application/json")
|
6
7
|
self.where = where
|
7
8
|
self.limit = limit
|
8
9
|
self.offset = offset
|
9
10
|
self.view = view
|
10
11
|
self.per_page = per_page
|
12
|
+
self.accept = accept
|
11
13
|
end
|
12
14
|
|
13
|
-
def merge(where: {}, limit: nil, offset: nil, view: nil, per_page: nil)
|
15
|
+
def merge(where: {}, limit: nil, offset: nil, view: nil, per_page: nil, accept: nil)
|
14
16
|
Constraints.new(where: where.merge(where),
|
15
17
|
limit: limit || self.limit,
|
16
18
|
per_page: per_page || self.per_page,
|
17
19
|
offset: offset || self.offset,
|
18
|
-
view: view || self.view
|
20
|
+
view: view || self.view,
|
21
|
+
accept: accept || self.accept)
|
19
22
|
end
|
23
|
+
# rubocop:enable Metrics/ParameterLists
|
20
24
|
|
21
25
|
def next_page
|
22
26
|
merge(offset: offset + per_page)
|
@@ -32,6 +36,10 @@ module Nylas
|
|
32
36
|
query
|
33
37
|
end
|
34
38
|
|
39
|
+
def to_headers
|
40
|
+
accept == "application/json" ? {} : { "Accept" => accept, "Content-types" => accept }
|
41
|
+
end
|
42
|
+
|
35
43
|
def limit_for_query
|
36
44
|
!limit.nil? && limit < per_page ? limit : per_page
|
37
45
|
end
|
data/lib/nylas/contact.rb
CHANGED
@@ -4,12 +4,20 @@ module Nylas
|
|
4
4
|
class Contact
|
5
5
|
include Model
|
6
6
|
self.resources_path = "/contacts"
|
7
|
+
self.creatable = true
|
8
|
+
self.listable = true
|
9
|
+
self.showable = true
|
10
|
+
self.filterable = true
|
11
|
+
self.updatable = true
|
12
|
+
self.destroyable = true
|
7
13
|
|
8
|
-
attribute :id, :string, exclude_when: %i
|
14
|
+
attribute :id, :string, exclude_when: %i[creating updating]
|
9
15
|
attribute :object, :string, default: "contact"
|
10
|
-
attribute :account_id, :string, exclude_when: %i
|
16
|
+
attribute :account_id, :string, exclude_when: %i[creating updating]
|
17
|
+
|
11
18
|
attribute :given_name, :string
|
12
19
|
attribute :middle_name, :string
|
20
|
+
attribute :picture_url, :string
|
13
21
|
attribute :surname, :string
|
14
22
|
attribute :birthday, :nylas_date
|
15
23
|
attribute :suffix, :string
|