gandi_v5 0.8.0 → 0.9.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -9
  3. data/README.md +54 -10
  4. data/lib/gandi_v5.rb +112 -66
  5. data/lib/gandi_v5/billing/info/prepaid.rb +1 -0
  6. data/lib/gandi_v5/data.rb +1 -0
  7. data/lib/gandi_v5/data/converter.rb +3 -2
  8. data/lib/gandi_v5/data/converter/array_of.rb +3 -2
  9. data/lib/gandi_v5/data/converter/integer.rb +3 -2
  10. data/lib/gandi_v5/data/converter/symbol.rb +3 -2
  11. data/lib/gandi_v5/data/converter/time.rb +3 -2
  12. data/lib/gandi_v5/domain.rb +9 -6
  13. data/lib/gandi_v5/domain/availability/product/period.rb +1 -1
  14. data/lib/gandi_v5/domain/contact.rb +5 -5
  15. data/lib/gandi_v5/domain/tld.rb +2 -2
  16. data/lib/gandi_v5/domain/transfer_in.rb +170 -0
  17. data/lib/gandi_v5/domain/transfer_in/availability.rb +51 -0
  18. data/lib/gandi_v5/email.rb +1 -0
  19. data/lib/gandi_v5/email/mailbox.rb +1 -1
  20. data/lib/gandi_v5/error/gandi_error.rb +1 -0
  21. data/lib/gandi_v5/live_dns.rb +2 -0
  22. data/lib/gandi_v5/live_dns/domain.rb +28 -1
  23. data/lib/gandi_v5/live_dns/domain/dnssec_key.rb +16 -11
  24. data/lib/gandi_v5/live_dns/domain/snapshot.rb +4 -0
  25. data/lib/gandi_v5/live_dns/domain/tsig_key.rb +7 -4
  26. data/lib/gandi_v5/simple_hosting.rb +1 -0
  27. data/lib/gandi_v5/simple_hosting/instance.rb +3 -0
  28. data/lib/gandi_v5/simple_hosting/instance/application.rb +1 -0
  29. data/lib/gandi_v5/simple_hosting/instance/database.rb +1 -0
  30. data/lib/gandi_v5/simple_hosting/instance/language.rb +1 -1
  31. data/lib/gandi_v5/simple_hosting/instance/upgrade.rb +1 -0
  32. data/lib/gandi_v5/simple_hosting/instance/virtual_host.rb +2 -2
  33. data/lib/gandi_v5/simple_hosting/instance/virtual_host/linked_dns_zone.rb +1 -0
  34. data/lib/gandi_v5/version.rb +1 -1
  35. data/spec/features/list_domain_renewals_spec.rb +16 -0
  36. data/spec/features/list_email_addresses_spec.rb +39 -0
  37. data/spec/fixtures/bodies/GandiV5_Domain_TransferIn/fetch.yml +21 -0
  38. data/spec/fixtures/bodies/GandiV5_Domain_TransferIn_Availability/fetch.yml +10 -0
  39. data/spec/fixtures/vcr/Examples/List_domain_renewals.yml +54 -0
  40. data/spec/fixtures/vcr/Examples/List_email_addresses.yml +103 -0
  41. data/spec/units/gandi_v5/domain/transfer_in/availability_spec.rb +49 -0
  42. data/spec/units/gandi_v5/domain/transfer_in_spec.rb +143 -0
  43. metadata +127 -20
  44. data/.gitignore +0 -26
  45. data/.rspec +0 -3
  46. data/.rubocop.yml +0 -74
  47. data/.travis.yml +0 -38
  48. data/FUNDING.yml +0 -10
  49. data/Gemfile +0 -6
  50. data/Guardfile +0 -39
  51. data/Rakefile +0 -3
  52. data/bin/console +0 -13
  53. data/gandi_v5.gemspec +0 -42
@@ -3,6 +3,7 @@
3
3
  class GandiV5
4
4
  # Addin providing a DSL to manage declaring attributes and how to map
5
5
  # and convert to/from Gandi's fields.
6
+ # @api private
6
7
  module Data
7
8
  # api private
8
9
  # Add contents of ClassMethods to the Class which includes this module.
@@ -3,6 +3,7 @@
3
3
  class GandiV5
4
4
  module Data
5
5
  # Namespace for converters to/from Gandi's format.
6
+ # @api private
6
7
  class Converter
7
8
  # Initialize a new simple converter.
8
9
  # The passed procs will be run at the appropriate time.
@@ -21,8 +22,8 @@ class GandiV5
21
22
  to_gandi_proc.call value
22
23
  end
23
24
 
24
- # @param [Object]
25
- # @return value [Object]
25
+ # @param value [Object]
26
+ # @return [Object]
26
27
  def from_gandi(value)
27
28
  return value unless from_gandi_proc
28
29
 
@@ -4,6 +4,7 @@ class GandiV5
4
4
  module Data
5
5
  class Converter
6
6
  # Used for applying the same converter to each item in an array.
7
+ # @api private
7
8
  class ArrayOf
8
9
  # @param converter the converter to apply to each item in the array.
9
10
  def initialize(converter)
@@ -18,8 +19,8 @@ class GandiV5
18
19
  value.map { |item| converter.to_gandi(item) }
19
20
  end
20
21
 
21
- # @param [Array<Object>]
22
- # @return value [Array<Object>]
22
+ # @param value [Array<Object>]
23
+ # @return [Array<Object>]
23
24
  def from_gandi(value)
24
25
  return nil if value.nil?
25
26
 
@@ -4,6 +4,7 @@ class GandiV5
4
4
  module Data
5
5
  class Converter
6
6
  # Methods for converting strings to/from integerss.
7
+ # @api private
7
8
  class Integer
8
9
  # @param value [Integer]
9
10
  # @return [String]
@@ -13,8 +14,8 @@ class GandiV5
13
14
  value.to_s
14
15
  end
15
16
 
16
- # @param [String]
17
- # @return value [Integer]
17
+ # @param value [String]
18
+ # @return [Integer]
18
19
  def self.from_gandi(value)
19
20
  return nil if value.nil?
20
21
 
@@ -4,6 +4,7 @@ class GandiV5
4
4
  module Data
5
5
  class Converter
6
6
  # Methods for converting strings to/from symbols.
7
+ # @api private
7
8
  class Symbol
8
9
  # @param value [Symbol]
9
10
  # @return [String]
@@ -13,8 +14,8 @@ class GandiV5
13
14
  value.to_s
14
15
  end
15
16
 
16
- # @param [String]
17
- # @return value [Symbol]
17
+ # @param value [String]
18
+ # @return [Symbol]
18
19
  def self.from_gandi(value)
19
20
  return nil if value.nil?
20
21
 
@@ -4,6 +4,7 @@ class GandiV5
4
4
  module Data
5
5
  class Converter
6
6
  # Methods for converting times to/from Gandi ("2019-02-13T11:04:18Z").
7
+ # @api private
7
8
  class Time
8
9
  # Convert a time to Gandi's prefered string format.
9
10
  # @param value [Time]
@@ -15,8 +16,8 @@ class GandiV5
15
16
  end
16
17
 
17
18
  # Convert a time from Gandi's prefered string format.
18
- # @param [String]
19
- # @return value [Time]
19
+ # @param value [String]
20
+ # @return [Time]
20
21
  def self.from_gandi(value)
21
22
  return nil if value.nil?
22
23
 
@@ -7,7 +7,7 @@ class GandiV5
7
7
  # @return [String] fully qualified domain name, written in its native alphabet (IDN).
8
8
  # @!attribute [r] fqdn_unicode
9
9
  # @return [String] fully qualified domain name, written in unicode.
10
- # @see https://docs.gandi.net/en/domain_names/register/idn.html
10
+ # @see https://docs.gandi.net/en/domain_names/register/idn.html
11
11
  # @!attribute [r] name_servers
12
12
  # @return [Array<String>]
13
13
  # @!attribute [r] services
@@ -20,7 +20,7 @@ class GandiV5
20
20
  # @return [String] one of: "clientHold", "clientUpdateProhibited", "clientTransferProhibited",
21
21
  # "clientDeleteProhibited", "clientRenewProhibited", "serverHold", "pendingTransfer",
22
22
  # "serverTransferProhibited"
23
- # @see https://docs.gandi.net/en/domain_names/faq/domain_statuses.html
23
+ # @see https://docs.gandi.net/en/domain_names/faq/domain_statuses.html
24
24
  # @!attribute [r] tld
25
25
  # @return [String]
26
26
  # @!attribute [r] dates
@@ -206,7 +206,7 @@ class GandiV5
206
206
  end
207
207
 
208
208
  # Renew domain.
209
- # Warning! This is not a free operation. Please ensure your prepaid account has enough credit.
209
+ # @note This is not a free operation. Please ensure your prepaid account has enough credit.
210
210
  # @see https://api.gandi.net/docs/domains#post-v5-domain-domains-domain-renew
211
211
  # @param duration [Integer, #to_s] how long to renew for (in years).
212
212
  # @return [String] confirmation message from Gandi.
@@ -242,7 +242,7 @@ class GandiV5
242
242
  end
243
243
 
244
244
  # Restore an expired domain.
245
- # Warning! This is not a free operation. Please ensure your prepaid account has enough credit.
245
+ # @note This is not a free operation. Please ensure your prepaid account has enough credit.
246
246
  # @see https://docs.gandi.net/en/domain_names/renew/restore.html
247
247
  # @see https://api.gandi.net/docs/domains#post-v5-domain-domains-domain-restore
248
248
  # @return [String] The confirmation message from Gandi.
@@ -399,23 +399,26 @@ class GandiV5
399
399
  data['message']
400
400
  end
401
401
 
402
+ # Get email mailboxes for the domain.
402
403
  # @see GandiV5::Email::Mailbox.list
403
404
  def mailboxes(**params)
404
405
  GandiV5::Email::Mailbox.list(**params, fqdn: fqdn)
405
406
  end
406
407
 
408
+ # Get email slots for the domain.
407
409
  # @see GandiV5::Email::Slot.list
408
410
  def mailbox_slots(**params)
409
411
  GandiV5::Email::Slot.list(**params, fqdn: fqdn)
410
412
  end
411
413
 
414
+ # Get email forwards for the domain.
412
415
  # @see GandiV5::Email::Forward.list
413
416
  def email_forwards(**params)
414
417
  GandiV5::Email::Forward.list(**params, fqdn: fqdn)
415
418
  end
416
419
 
417
420
  # Create (register) a new domain.
418
- # Warning! This is not a free operation. Please ensure your prepaid account has enough credit.
421
+ # @note This is not a free operation. Please ensure your prepaid account has enough credit.
419
422
  # @see https://api.gandi.net/docs/domains#post-v5-domain-domains
420
423
  # @param fqdn [String, #to_s] the fully qualified domain name to create.
421
424
  # @param dry_run [Boolean]
@@ -459,7 +462,7 @@ class GandiV5
459
462
  # Contents of a Signed Mark Data file (used for newgtld sunrises, tld_period must be sunrise).
460
463
  # @param tld_period ["sunrise", "landrush", "eap1", "eap2", "eap3", "eap4", "eap5", "eap6",
461
464
  # "eap7", "eap8", "eap9", "golive", #to_gandi, #to_json] (optional)
462
- # @see https://docs.gandi.net/en/domain_names/register/new_gtld.html
465
+ # @see https://docs.gandi.net/en/domain_names/register/new_gtld.html
463
466
  # @return [GandiV5::Domain] the created domain
464
467
  # @return [Hash] if doing a dry run, you get what Gandi returns
465
468
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error
@@ -4,7 +4,7 @@ class GandiV5
4
4
  class Domain
5
5
  class Availability
6
6
  class Product
7
- # Information about an available product.
7
+ # Information about an available product's period.
8
8
  # @!attribute [r] name
9
9
  # @return [String]
10
10
  # @!attribute [r]
@@ -4,15 +4,15 @@ class GandiV5
4
4
  class Domain
5
5
  # Information for a single contact for a domain.
6
6
  # @!attribute [r] country
7
- # @return [String] .
7
+ # @return [String]
8
8
  # @!attribute [r] email
9
- # @return [String] .
9
+ # @return [String]
10
10
  # @!attribute [r] family
11
- # @return [String] .
11
+ # @return [String]
12
12
  # @!attribute [r] given
13
- # @return [String] .
13
+ # @return [String]
14
14
  # @!attribute [r] address
15
- # @return [String] .
15
+ # @return [String]
16
16
  # @!attribute [r] type
17
17
  # @return [:person, :company, :association, :'public body', :reseller]
18
18
  # @!attribute [r] brand_number
@@ -12,7 +12,7 @@ class GandiV5
12
12
  # @!attribute [r] category
13
13
  # @return [String]
14
14
  # @!attribute [r] change_owner
15
- # @return [Boolean] whther changing owner is pemritted.
15
+ # @return [Boolean] whether changing owner is pemritted.
16
16
  # @!attribute [r] corporate
17
17
  # @return [Boolean] whether this is a corporate TLD.
18
18
  # @!attribute [r] ext_trade
@@ -27,7 +27,7 @@ class GandiV5
27
27
 
28
28
  # List of available TLDs.
29
29
  # @see https://api.gandi.net/docs/domains#get-v5-domain-tlds
30
- # @return Array<GandiV5::Domain::TLD>
30
+ # @return [Array<GandiV5::Domain::TLD>]
31
31
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
32
32
  def self.list
33
33
  GandiV5.get(url)
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class Domain
5
+ # Manage a domain's transfer to Gandi.
6
+ # @!attribute [r] fqdn
7
+ # @return [String] the fully qualified domain name being transfered
8
+ # @!attribute [r] created_at
9
+ # @return [Time]
10
+ # @!attribute [r] updated_at
11
+ # @return [Time]
12
+ # @!attribute [r] owner_contact
13
+ # @return [String]
14
+ # @!attribute [r] step
15
+ # @return [String]
16
+ # @!attribute [r] step_number
17
+ # @return [Integer]
18
+ # @!attribute [r] errortype
19
+ # @return [String] (optional)
20
+ # @!attribute [r] errortype_label
21
+ # @return [String] (optional)
22
+ # @!attribute [r] duration
23
+ # @return [Integer] (optional)
24
+ # @!attribute [r] reseller_uuid
25
+ # @return [String] (optional)
26
+ # @!attribute [r] version
27
+ # @return [Integer] (optional)
28
+ # @!attribute [r] foa
29
+ # @return [Hash{String=>String}] (optional) email => status
30
+ # @!attribute [r] inner_step
31
+ # @return [String] (optional)
32
+ # @!attribute [r] transfer_procedure
33
+ # @return [String] (optional)
34
+ # @!attribute [r] start_at
35
+ # @return [Time] (optional)
36
+ # @!attribute [r] regac_at
37
+ # @return [Time] (optional)
38
+ class TransferIn
39
+ include GandiV5::Data
40
+
41
+ members :fqdn, :owner_contact, :step, :errortype, :errortype_label, :inner_step,
42
+ :transfer_procedure, :reseller_uuid, :version, :foa_status, :duration
43
+
44
+ member :step_number, gandi_key: 'step_nb'
45
+ member :created_at, converter: GandiV5::Data::Converter::Time
46
+ member :updated_at, converter: GandiV5::Data::Converter::Time
47
+ member :regac_at, converter: GandiV5::Data::Converter::Time
48
+ member :start_at, converter: GandiV5::Data::Converter::Time
49
+
50
+ # Relaunch the transfer process after something went wrong.
51
+ # @see https://api.gandi.net/docs/domains/#put-v5-domain-transferin-domain
52
+ # @return [String] the confirmation message from Gandi
53
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
54
+ def relaunch
55
+ TransferIn.relaunch fqdn
56
+ end
57
+
58
+ # Resend the {https://icannwiki.org/FOA Form Of Authorization} email.
59
+ # @see https://api.gandi.net/docs/domains/#post-v5-domain-transferin-domain-foa
60
+ # @return [String] the confirmation message from Gandi
61
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
62
+ def resend_foa_emails(email_address)
63
+ TransferIn.resend_foa_emails fqdn, email_address
64
+ end
65
+
66
+ # Start the transfer of a domain to Gandi.
67
+ # @note This is not a free operation. Please ensure your prepaid account has enough credit.
68
+ # @see https://api.gandi.net/docs/domains/#post-v5-domain-transferin
69
+ # @param fqdn [String, #to_s] the fully qualified domain name to create.
70
+ # @param dry_run [Boolean]
71
+ # whether the details should be checked instead of actually creating the domain.
72
+ # @param sharing_id [String] either:
73
+ # * nil (default) - nothing special happens
74
+ # * an organization ID - pay using another organization
75
+ # (you need to have billing permissions on the organization
76
+ # and use the same organization name for the domain name's owner).
77
+ # The invoice will be edited using this organization's information.
78
+ # * a reseller ID - buy a domain for a customer using a reseller account
79
+ # (you need to have billing permissions on the reseller organization
80
+ # and have your customer's information for the owner).
81
+ # The invoice will be edited using the reseller organization's information.
82
+ # @param owner [GandiV5::Domain::Contact, #to_gandi, #to_h] (required)
83
+ # the owner of the new domain.
84
+ # @param admin [GandiV5::Domain::Contact, #to_gandi, #to_h] (optional, defaults to owner)
85
+ # the administrative contact for the new domain.
86
+ # @param bill [GandiV5::Domain::Contact, #to_gandi, #to_h] (optional, defaults to owner)
87
+ # the billing contact for the new domain.
88
+ # @param tech [GandiV5::Domain::Contact, #to_gandi, #to_h] (optional, defaults to owner)
89
+ # the technical contact for the new domain.
90
+ # @param currency ["EUR", "USD", "GBP", "TWD", "CNY"] (optional)
91
+ # the currency you wish to be charged in.
92
+ # @param duration [Integer] (optional, default 1, minimum 1 maximum 10)
93
+ # how many years to register for.
94
+ # @param enforce_premium [Boolean] (optional)
95
+ # must be set to true if the domain is a premium domain.
96
+ # @param extra_parameters [Hash, #to_gandi, #to_json] (optional)
97
+ # unknown - not documented at Gandi.
98
+ # @param nameserver_ips [Hash<String => Array<String>>, #to_gandi, #to_json] (optional)
99
+ # For glue records only - dictionnary associating a nameserver to a list of IP addresses.
100
+ # @param nameservers [Array<String>, #to_gandi, #to_json] (optional)
101
+ # List of nameservers. Gandi's LiveDNS nameservers are used if omitted..
102
+ # @param price [Numeric, #to_gandi, #to_json] (optional) unknown - not documented at Gandi.
103
+ # @param resellee_id [String, #to_gandi, #to_json] (optional)
104
+ # unknown - not documented at Gandi.
105
+ # @param change_owner [Boolean] (optional)
106
+ # whether the change the domain's owner during the transfer.
107
+ # @param auth_code [String] (optional) authorization code (if required).
108
+ # @return [String] the confirmation message from Gandi
109
+ # @return [Hash] if doing a dry run, you get what Gandi returns
110
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error
111
+ def self.create(fqdn, dry_run: false, sharing_id: nil, **params)
112
+ fail ArgumentError, 'missing keyword: owner' unless params.key?(:owner)
113
+
114
+ body = params.merge(fqdn: fqdn)
115
+ .transform_values { |val| val.respond_to?(:to_gandi) ? val.to_gandi : val }
116
+ .to_json
117
+ url_ = sharing_id ? "#{url}?sharing_id=#{sharing_id}" : url
118
+
119
+ _response, data = GandiV5.post(url_, body, 'Dry-Run': dry_run ? 1 : 0)
120
+ dry_run ? data : data['message']
121
+ end
122
+
123
+ # Get information on an existing transfer.
124
+ # @see https://api.gandi.net/docs/domains/#get-v5-domain-transferin-domain
125
+ # @param fqdn [String, #to_s] the fully qualified domain name to check.
126
+ # @return [GandiV5::Domain::TransferIn]
127
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
128
+ def self.fetch(fqdn)
129
+ _response, data = GandiV5.get url(fqdn)
130
+ transfer = from_gandi data
131
+ transfer.instance_exec { @fqdn = data.dig('params', 'domain') }
132
+ transfer.instance_exec { @reseller_uuid = data.dig('params', 'reseller') }
133
+ transfer.instance_exec { @version = data.dig('params', 'version') }
134
+ transfer.instance_exec { @duration = data.dig('params', 'duration') }
135
+ if data.key?('foa')
136
+ transfer.instance_exec do
137
+ @foa_status = Hash[data['foa'].map { |hash| hash.values_at('email', 'answer') }]
138
+ end
139
+ end
140
+ transfer
141
+ end
142
+
143
+ # Relaunch the transfer process after something went wrong.
144
+ # @see https://api.gandi.net/docs/domains/#put-v5-domain-transferin-domain
145
+ # @return [String] the confirmation message from Gandi
146
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
147
+ def self.relaunch(fqdn)
148
+ _response, data = GandiV5.put url(fqdn)
149
+ data['message']
150
+ end
151
+
152
+ # Resend the {https://icannwiki.org/FOA Form Of Authorization} email.
153
+ # @see https://api.gandi.net/docs/domains/#post-v5-domain-transferin-domain-foa
154
+ # @return [String] the confirmation message from Gandi
155
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
156
+ def self.resend_foa_emails(fqdn, email_address)
157
+ _response, data = GandiV5.post "#{url(fqdn)}/foa", { 'email' => email_address }.to_json
158
+ data['message']
159
+ end
160
+
161
+ private
162
+
163
+ def self.url(fqdn = nil)
164
+ "#{BASE}domain/transferin" +
165
+ (fqdn ? "/#{CGI.escape fqdn}" : '')
166
+ end
167
+ private_class_method :url
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class Domain
5
+ class TransferIn
6
+ # Information about the availabillity of a domain to be transfered into Gandi.
7
+ # @!attribute [r] fqdn
8
+ # @return [String] the fully qualified domain name.
9
+ # @!attribute [r] fqdn_unicode
10
+ # @return [String] the fully qualified domain name in unicode.
11
+ # @!attribute [r] available
12
+ # @return [Boolean] whether the domain can be transfered.
13
+ # @!attribute [r] corporate
14
+ # @return [Boolean] Optional
15
+ # @!attribute [r] internal
16
+ # @return [Boolean] Optional
17
+ # @!attribute [r] minimum_duration
18
+ # @return [Integer] Optional the minimum duration you can reregister the domain for.
19
+ # @!attribute [r] maximum_duration
20
+ # @return [Integer] Optional the maximum duration you can reregister the domain for.
21
+ # @!attribute [r] durations
22
+ # @return [Array<Integer>] Optional the durations you can reregister the domain for.
23
+ # @!attribute [r] message
24
+ # @return [String, nil] Optional message explaining why the domain can't be transfered.
25
+ class Availability
26
+ include GandiV5::Data
27
+
28
+ members :fqdn, :available, :corporate, :internal,
29
+ :minimum_duration, :maximum_duration
30
+ member :durations, array: true
31
+ member :message, gandi_key: 'msg'
32
+ member :fqdn_unicode, gandi_key: 'fqdn_ulabel'
33
+
34
+ # Find out if a domain can be transfered to Gandi.
35
+ # @see https://api.gandi.net/docs/domains/#post-v5-domain-transferin-domain-available
36
+ # @param fqdn [String, #to_s] the fully qualified domain name to query.
37
+ # @param auth_code [String, #to_s] authorization code (if required).
38
+ # @return [GandiV5::Domain::TransferIn::Availabillity]
39
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
40
+ def self.fetch(fqdn, auth_code = nil)
41
+ url = "#{BASE}domain/transferin/#{fqdn}/available"
42
+ body = {}
43
+ body['authinfo'] = auth_code if auth_code
44
+
45
+ _response, data = GandiV5.post url, body
46
+ from_gandi data
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end