serdee 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +54 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/address.rb +31 -0
- data/examples/request_message.json +31 -0
- data/examples/request_message.rb +9 -0
- data/examples/response_code.rb +79 -0
- data/examples/response_message.json +36 -0
- data/examples/response_message.rb +24 -0
- data/examples/unified_message.rb +96 -0
- data/lib/serdee/attribute.rb +100 -0
- data/lib/serdee/attributes.rb +154 -0
- data/lib/serdee/version.rb +3 -0
- data/lib/serdee.rb +10 -0
- data/serdee.gemspec +30 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d32e12c6af56fcee2b79e0d1c3390410459d5c23
|
4
|
+
data.tar.gz: 0e8003cbd25ad282db4749c35119326c3cb23f0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 97c96e37529521be72bd5ffe824a6f0cc11f07e64e6c838ae7719c32989cfe2b12be649d11b52a4f8144eee88f33dfe8620051d7b8ccff352a5b128d8598f7fe
|
7
|
+
data.tar.gz: '0818da033b2801fcf157d2be3f753c5bb61b87385131694c4ae3ba734086d54063bf9a7ac56793a5c68bb9beb7c95809e16255607a8a5ac56f2810db18ad02fb'
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
serdee (0.1.0)
|
5
|
+
activesupport
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (5.2.1)
|
11
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
|
+
i18n (>= 0.7, < 2)
|
13
|
+
minitest (~> 5.1)
|
14
|
+
tzinfo (~> 1.1)
|
15
|
+
coderay (1.1.2)
|
16
|
+
concurrent-ruby (1.0.5)
|
17
|
+
diff-lcs (1.3)
|
18
|
+
i18n (1.1.0)
|
19
|
+
concurrent-ruby (~> 1.0)
|
20
|
+
method_source (0.9.0)
|
21
|
+
minitest (5.11.3)
|
22
|
+
pry (0.11.2)
|
23
|
+
coderay (~> 1.1.0)
|
24
|
+
method_source (~> 0.9.0)
|
25
|
+
rake (10.5.0)
|
26
|
+
rspec (3.8.0)
|
27
|
+
rspec-core (~> 3.8.0)
|
28
|
+
rspec-expectations (~> 3.8.0)
|
29
|
+
rspec-mocks (~> 3.8.0)
|
30
|
+
rspec-core (3.8.0)
|
31
|
+
rspec-support (~> 3.8.0)
|
32
|
+
rspec-expectations (3.8.1)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.8.0)
|
35
|
+
rspec-mocks (3.8.0)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.8.0)
|
38
|
+
rspec-support (3.8.0)
|
39
|
+
thread_safe (0.3.6)
|
40
|
+
tzinfo (1.2.5)
|
41
|
+
thread_safe (~> 0.1)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
bundler (~> 1.16)
|
48
|
+
pry
|
49
|
+
rake (~> 10.0)
|
50
|
+
rspec (~> 3.0)
|
51
|
+
serdee!
|
52
|
+
|
53
|
+
BUNDLED WITH
|
54
|
+
1.16.0
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Serdee
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/serdee`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'serdee'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install serdee
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/serdee.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "serdee"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/examples/address.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Address
|
3
|
+
BYTES = {
|
4
|
+
address: 22,
|
5
|
+
city: 13,
|
6
|
+
state: 3,
|
7
|
+
country: 2
|
8
|
+
}.freeze
|
9
|
+
attr_accessor :address, :city, :state, :country
|
10
|
+
def initialize(address:, city:, state:, country:)
|
11
|
+
@address = address
|
12
|
+
@city = city
|
13
|
+
@state = state
|
14
|
+
@country = country
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.serialize(address)
|
18
|
+
return if address.nil?
|
19
|
+
BYTES.map do |field, bytes|
|
20
|
+
address.send(field)[0...bytes].ljust(bytes, " ")
|
21
|
+
end.join
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.deserialize(string)
|
25
|
+
return if string.nil?
|
26
|
+
string = string.dup
|
27
|
+
new(
|
28
|
+
BYTES.transform_values { |bytes| string.slice!(0...bytes) }
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
{
|
2
|
+
"request":{
|
3
|
+
"header":{
|
4
|
+
"signature":"BHNUMS",
|
5
|
+
"details":{
|
6
|
+
"productCategoryCode":"01",
|
7
|
+
"specVersion":"04"
|
8
|
+
}
|
9
|
+
},
|
10
|
+
"transaction":{
|
11
|
+
"primaryAccountNumber":"9877890000000000",
|
12
|
+
"processingCode":"315400",
|
13
|
+
"transactionAmount":"000000000000",
|
14
|
+
"transmissionDateTime":"131121105414",
|
15
|
+
"systemTraceAuditNumber":"000110",
|
16
|
+
"localTransactionTime":"105414",
|
17
|
+
"localTransactionDate":"131121",
|
18
|
+
"merchantCategoryCode":"5411",
|
19
|
+
"pointOfServiceEntryMode":"011",
|
20
|
+
"acquiringInstitutionIdentifier":"60300000063",
|
21
|
+
"retrievalReferenceNumber":"000000002962",
|
22
|
+
"merchantTerminalId":"06220 600 ",
|
23
|
+
"merchantIdentifier":"60300000063 ",
|
24
|
+
"merchantLocation":"BLACKHAWK SIMULATOR PLEASANTON CA US",
|
25
|
+
"transactionCurrencyCode":"840",
|
26
|
+
"additionalTxnFields":{
|
27
|
+
"productId":"07675004390"
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class ResponseCode
|
3
|
+
attr_reader :code, :type, :message
|
4
|
+
def initialize(code:, type:, message:)
|
5
|
+
@code = code
|
6
|
+
@type = type
|
7
|
+
@message = message
|
8
|
+
end
|
9
|
+
|
10
|
+
def approved?
|
11
|
+
type == :approved
|
12
|
+
end
|
13
|
+
|
14
|
+
def soft_declined?
|
15
|
+
type == :soft_declined
|
16
|
+
end
|
17
|
+
|
18
|
+
def hard_declined?
|
19
|
+
type == :hard_declined
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find(code)
|
23
|
+
response = CODES.fetch(code)
|
24
|
+
new(
|
25
|
+
code: code,
|
26
|
+
type: response[0],
|
27
|
+
message: response[1]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
CODES = {
|
32
|
+
"00" => [:approved, "Approved – balance available"],
|
33
|
+
"01" => [:approved, "Approved – balance unavailable"],
|
34
|
+
"03" => [:approved, "Approved – balance unavailable on external account number"],
|
35
|
+
"74" => [:soft_declined, "Unable to route / System Error"],
|
36
|
+
"15" => [:soft_declined, "Time Out occurred- Auth Server not available /responding"],
|
37
|
+
"02" => [:hard_declined, "Refer to card issuer"],
|
38
|
+
"04" => [:hard_declined, "Already Redeemed"],
|
39
|
+
"05" => [:hard_declined, "Error account problem"],
|
40
|
+
"06" => [:hard_declined, "Invalid expiration date"],
|
41
|
+
"07" => [:hard_declined, "Unable to process"],
|
42
|
+
"08" => [:hard_declined, "Card not found"],
|
43
|
+
"12" => [:hard_declined, "Invalid transaction"],
|
44
|
+
"13" => [:hard_declined, "Invalid amount"],
|
45
|
+
"14" => [:hard_declined, "Invalid Product"],
|
46
|
+
"16" => [:hard_declined, "Invalid status change"],
|
47
|
+
"17" => [:hard_declined, "Invalid merchant"],
|
48
|
+
"18" => [:hard_declined, "Invalid Phone Number"],
|
49
|
+
"20" => [:hard_declined, "Invalid Pin"],
|
50
|
+
"21" => [:hard_declined, "Card already active"],
|
51
|
+
"22" => [:hard_declined, "Card Already Associated"],
|
52
|
+
"30" => [:hard_declined, "Bad track2 – format error"],
|
53
|
+
"33" => [:hard_declined, "Expired card"],
|
54
|
+
"34" => [:hard_declined, "Already reversed"],
|
55
|
+
"35" => [:hard_declined, "Already voided"],
|
56
|
+
"36" => [:hard_declined, "Restricted card"],
|
57
|
+
"37" => [:hard_declined, "Restricted External Account"],
|
58
|
+
"38" => [:hard_declined, "Restricted Merchant"],
|
59
|
+
"41" => [:hard_declined, "Lost card"],
|
60
|
+
"42" => [:hard_declined, "Lost External Account"],
|
61
|
+
"43" => [:hard_declined, "Stolen card"],
|
62
|
+
"44" => [:hard_declined, "Stolen External Account"],
|
63
|
+
"51" => [:hard_declined, "Insufficient funds"],
|
64
|
+
"54" => [:hard_declined, "Expired External Account"],
|
65
|
+
"55" => [:hard_declined, "Max recharge reached"],
|
66
|
+
"56" => [:hard_declined, "Advance less amount / enter lesser amount"],
|
67
|
+
"58" => [:hard_declined, "Request not permitted by merchant location"],
|
68
|
+
"59" => [:hard_declined, "Request not permitted by processor"],
|
69
|
+
"61" => [:hard_declined, "Exceeds withdrawal amt / over limit"],
|
70
|
+
"62" => [:hard_declined, "Exceeds financial limit"],
|
71
|
+
"65" => [:hard_declined, "Exceeds withdrawal frequency limit"],
|
72
|
+
"66" => [:hard_declined, "Exceeds transaction count limit"],
|
73
|
+
"69" => [:hard_declined, "Format error –bad data"],
|
74
|
+
"71" => [:hard_declined, "Invalid External Account number"],
|
75
|
+
"94" => [:hard_declined, "Duplicate transaction"],
|
76
|
+
"95" => [:hard_declined, "Cannot Reverse the Original Transaction"],
|
77
|
+
"99" => [:hard_declined, "General decline"],
|
78
|
+
}.freeze
|
79
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
{
|
2
|
+
"response":{
|
3
|
+
"header":{
|
4
|
+
"signature":"BHNUMS",
|
5
|
+
"details":{
|
6
|
+
"productCategoryCode":"01",
|
7
|
+
"statusCode":"00",
|
8
|
+
"specVersion":"04"
|
9
|
+
}
|
10
|
+
},
|
11
|
+
"transaction":{
|
12
|
+
"primaryAccountNumber":"9877890000000000",
|
13
|
+
"processingCode":"315400",
|
14
|
+
"transactionAmount":"000000000000",
|
15
|
+
"transmissionDateTime":"131121105414",
|
16
|
+
"systemTraceAuditNumber":"000110",
|
17
|
+
"localTransactionTime":"105414",
|
18
|
+
"localTransactionDate":"131121",
|
19
|
+
"merchantCategoryCode":"5411",
|
20
|
+
"pointOfServiceEntryMode":"011",
|
21
|
+
"acquiringInstitutionIdentifier":"60300000063",
|
22
|
+
"authidentificationResponse":"123456",
|
23
|
+
"responseCode":"00",
|
24
|
+
"retrievalReferenceNumber":"000000002962",
|
25
|
+
"merchantTerminalId":"06220 600 ",
|
26
|
+
"merchantIdentifier":"60300000063 ",
|
27
|
+
"transactionCurrencyCode":"840",
|
28
|
+
"additionalTxnFields":{
|
29
|
+
"productId":"07675004390",
|
30
|
+
"balanceAmount":"C000000002500",
|
31
|
+
"transactionUniqueId": "0JPKA8FKHT2H011784WA0524PM",
|
32
|
+
"correlatedTransactionUniqueId": "0JPKA8FKHT2H011784WA0524PM"
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./unified_message"
|
3
|
+
require_relative "./response_code"
|
4
|
+
class ResponseMessage
|
5
|
+
include Serdee::Attributes
|
6
|
+
set_key_transform :camel_lower
|
7
|
+
|
8
|
+
attr_accessor :request_id
|
9
|
+
|
10
|
+
nested :response, UnifiedMessage
|
11
|
+
|
12
|
+
def parsed_message?
|
13
|
+
status_code == "00" if status_code
|
14
|
+
end
|
15
|
+
|
16
|
+
def status
|
17
|
+
return unless response_code
|
18
|
+
@status ||= ResponseCode.find(response_code)
|
19
|
+
end
|
20
|
+
|
21
|
+
def approved?
|
22
|
+
status&.approved?
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./address"
|
3
|
+
require_relative "./response_code"
|
4
|
+
|
5
|
+
class UnifiedMessage
|
6
|
+
include Serdee::Attributes
|
7
|
+
set_key_transform :camel_lower
|
8
|
+
|
9
|
+
PROCESSING_CODES = {
|
10
|
+
balance_inquiry: "315400",
|
11
|
+
redemption: "615400"
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
nested :header do
|
15
|
+
attribute :signature, default: "BHNUMS"
|
16
|
+
nested :details do
|
17
|
+
attribute :product_category_code, default: "01"
|
18
|
+
attribute :spec_version, default: "43"
|
19
|
+
attribute :status_code
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
nested :transaction do
|
24
|
+
attribute :local_transaction_at do
|
25
|
+
insert do |time, hash|
|
26
|
+
if time
|
27
|
+
hash[:local_transaction_time] = time.strftime("%H%M%S")
|
28
|
+
hash[:local_transaction_date] = time.strftime("%y%m%d")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
extract do |data|
|
33
|
+
if data[:local_transaction_date] && data[:local_transaction_time]
|
34
|
+
time_string = "#{data[:local_transaction_date]}-#{data[:local_transaction_time]}"
|
35
|
+
DateTime.strptime(time_string, "%y%m%d-%H%M%S")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
attribute :retrieval_reference_number do
|
40
|
+
serialize { |int| format("%012d", int) }
|
41
|
+
deserialize { |string| string&.to_i }
|
42
|
+
end
|
43
|
+
attribute :system_trace_audit_number do
|
44
|
+
serialize { |int| format("%06d", int) }
|
45
|
+
deserialize { |string| string&.to_i }
|
46
|
+
end
|
47
|
+
attribute :transaction_amount do
|
48
|
+
serialize { |int| format("%012d", int) }
|
49
|
+
deserialize { |string| string&.to_i }
|
50
|
+
end
|
51
|
+
attribute :transmission_date_time do
|
52
|
+
serialize { |time| time&.strftime("%y%m%d%H%M%S") }
|
53
|
+
deserialize { |time_string| DateTime.strptime(time_string, "%y%m%d%H%M%S") if time_string }
|
54
|
+
end
|
55
|
+
attribute :merchant_identifier do
|
56
|
+
serialize { |id| id&.ljust(15, " ") }
|
57
|
+
deserialize { |id| id&.strip }
|
58
|
+
end
|
59
|
+
attribute :action, as: :processing_code do
|
60
|
+
serialize { |action| PROCESSING_CODES.fetch(action) }
|
61
|
+
deserialize { |code| PROCESSING_CODES.key(code) }
|
62
|
+
end
|
63
|
+
attribute :merchant_location, serializer: Address
|
64
|
+
|
65
|
+
attribute :authidentification_response
|
66
|
+
attribute :acquiring_institution_identifier
|
67
|
+
attribute :merchant_category_code
|
68
|
+
attribute :merchant_terminal_id
|
69
|
+
attribute :point_of_service_entry_mode
|
70
|
+
attribute :primary_account_number
|
71
|
+
attribute :response_code
|
72
|
+
attribute :transaction_currency_code
|
73
|
+
|
74
|
+
nested :additional_txn_fields do
|
75
|
+
attribute :balance_amount do
|
76
|
+
serialize do |int|
|
77
|
+
return if int.nil?
|
78
|
+
(int >= 0 ? "C" : "D") + int.abs.to_s.rjust(12, "0")
|
79
|
+
end
|
80
|
+
deserialize do |string|
|
81
|
+
return if string.nil?
|
82
|
+
int = string[1..-1].to_i
|
83
|
+
return -int if string[0] == "D"
|
84
|
+
int
|
85
|
+
end
|
86
|
+
end
|
87
|
+
attribute :correlated_transaction_unique_id
|
88
|
+
attribute :product_id
|
89
|
+
attribute :transaction_unique_id
|
90
|
+
attribute :redemption_pin do
|
91
|
+
serialize { |pin| pin&.ljust(16, " ") }
|
92
|
+
deserialize { |pin| pin&.strip }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Serdee
|
3
|
+
class Attribute
|
4
|
+
attr_reader :key, :default
|
5
|
+
attr_writer :as
|
6
|
+
attr_accessor :default, :allow_blank, :serializer, :optional
|
7
|
+
|
8
|
+
def initialize(key, **options, &block)
|
9
|
+
@key = key
|
10
|
+
{ allow_blank: false }.merge(options).each do |option, setting|
|
11
|
+
config.public_send(option, setting)
|
12
|
+
end
|
13
|
+
config.instance_eval(&block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
@config ||= Config.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def as
|
21
|
+
@as || key
|
22
|
+
end
|
23
|
+
|
24
|
+
def default=(val)
|
25
|
+
@optional = true
|
26
|
+
@default = val
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert(value, hash)
|
30
|
+
new_value = serialize(value)
|
31
|
+
hash[as] = new_value if new_value.present? || allow_blank
|
32
|
+
end
|
33
|
+
|
34
|
+
def insert_to(obj, hash)
|
35
|
+
insert(obj.send(key), hash)
|
36
|
+
hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_to(data, obj)
|
40
|
+
obj.send("#{key}=", extract(data))
|
41
|
+
obj
|
42
|
+
end
|
43
|
+
|
44
|
+
def extract(hash)
|
45
|
+
deserialize(hash[as])
|
46
|
+
end
|
47
|
+
|
48
|
+
def serialize(value)
|
49
|
+
value
|
50
|
+
end
|
51
|
+
|
52
|
+
def deserialize(value)
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
class Config
|
57
|
+
attr_reader :attribute
|
58
|
+
def initialize(attribute)
|
59
|
+
@attribute = attribute
|
60
|
+
end
|
61
|
+
|
62
|
+
def serializer(obj)
|
63
|
+
return unless obj
|
64
|
+
|
65
|
+
attribute.serializer = obj
|
66
|
+
serialize { |value| obj.serialize(value) }
|
67
|
+
deserialize { |value| obj.deserialize(value) }
|
68
|
+
end
|
69
|
+
|
70
|
+
[
|
71
|
+
:optional,
|
72
|
+
:default,
|
73
|
+
:allow_blank,
|
74
|
+
:as
|
75
|
+
].each do |key|
|
76
|
+
define_method(key) do |value|
|
77
|
+
attribute.send("#{key}=", value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
[
|
82
|
+
:extract_to,
|
83
|
+
:insert_to,
|
84
|
+
:extract,
|
85
|
+
:insert,
|
86
|
+
:serialize,
|
87
|
+
:deserialize
|
88
|
+
].each do |key|
|
89
|
+
expected_arg_size = Attribute.instance_method(key).parameters.size
|
90
|
+
define_method(key) do |&block|
|
91
|
+
if block.parameters.size > expected_arg_size || block.parameters.any? { |type, _key| type == :keyreq }
|
92
|
+
raise ArgumentError,
|
93
|
+
"wrong number of arguments defined for ##{key} (given #{block.parameters}, expected #{args})"
|
94
|
+
end
|
95
|
+
attribute.singleton_class.send(:define_method, key, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_support/core_ext/hash"
|
3
|
+
|
4
|
+
module Serdee
|
5
|
+
module Attributes
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :deserialized_from
|
11
|
+
|
12
|
+
def initialize(**values)
|
13
|
+
self.class.attributes.each do |key, attribute|
|
14
|
+
if !attribute.optional && !values.key?(key)
|
15
|
+
raise ArgumentError, "#{key} is required parameter"
|
16
|
+
end
|
17
|
+
|
18
|
+
if values.key?(key)
|
19
|
+
send("#{key}=", values[key])
|
20
|
+
elsif attribute.default
|
21
|
+
send("#{key}=", attribute.default)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes
|
27
|
+
self.class.attributes.keys.each_with_object({}) do |key, hash|
|
28
|
+
value = send(key)
|
29
|
+
hash[key] = value if value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_json
|
34
|
+
self.class.insert_to(self, {}).deep_transform_keys! do |key|
|
35
|
+
if self.class.key_transform
|
36
|
+
self.class.key_transform.call(key.to_s)
|
37
|
+
else
|
38
|
+
key.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json
|
44
|
+
as_json.to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def attributes
|
49
|
+
@attributes ||= {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def serializers
|
53
|
+
@serializers ||= {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_key_transform(transform_name = nil, &block)
|
57
|
+
if transform_name
|
58
|
+
mapping = {
|
59
|
+
camel: :camelize.to_proc,
|
60
|
+
camel_lower: ->(key) { key.camelize(:lower) },
|
61
|
+
dash: :dasherize.to_proc,
|
62
|
+
underscore: :underscore.to_proc
|
63
|
+
}
|
64
|
+
|
65
|
+
@key_transform = mapping[transform_name.to_sym]
|
66
|
+
elsif block
|
67
|
+
@key_transform = block
|
68
|
+
else
|
69
|
+
raise ArgumentError, "must supply at least transform_name or block"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def key_transform
|
74
|
+
@key_transform || Serdee.default_key_transform
|
75
|
+
end
|
76
|
+
|
77
|
+
def attributes_class
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute(key, *args, &block)
|
82
|
+
attributes_class.attributes[key] = serializers[key] =
|
83
|
+
Attribute.new(key, *args, &block)
|
84
|
+
|
85
|
+
attributes_class.send(:attr_accessor, key)
|
86
|
+
end
|
87
|
+
|
88
|
+
def nested(key, nested_class = nil, &block)
|
89
|
+
serializers[key] = Nested.new(
|
90
|
+
attributes_class,
|
91
|
+
key,
|
92
|
+
nested_class&.serializers,
|
93
|
+
&block
|
94
|
+
)
|
95
|
+
|
96
|
+
return unless nested_class
|
97
|
+
|
98
|
+
attributes_class.attributes.merge!(nested_class.attributes)
|
99
|
+
attributes_class.send(:attr_accessor, *nested_class.attributes.keys)
|
100
|
+
end
|
101
|
+
|
102
|
+
def from_json(json)
|
103
|
+
of_json(JSON.parse(json))
|
104
|
+
end
|
105
|
+
|
106
|
+
def of_json(json)
|
107
|
+
allocate.tap do |obj|
|
108
|
+
data = json.deep_transform_keys do |key|
|
109
|
+
key.underscore.to_sym
|
110
|
+
end
|
111
|
+
extract_to(data, obj)
|
112
|
+
obj.deserialized_from = json
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def extract_to(data, obj)
|
117
|
+
return if obj.nil?
|
118
|
+
serializers.each do |_key, attribute|
|
119
|
+
attribute.extract_to(data, obj)
|
120
|
+
end
|
121
|
+
obj
|
122
|
+
end
|
123
|
+
|
124
|
+
def insert_to(obj, data)
|
125
|
+
serializers.each do |_key, attribute|
|
126
|
+
attribute.insert_to(obj, data)
|
127
|
+
end
|
128
|
+
data
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Nested
|
133
|
+
include Attributes::ClassMethods
|
134
|
+
|
135
|
+
attr_reader :key, :attributes_class
|
136
|
+
def initialize(attributes_class, key, serializers, &block)
|
137
|
+
@attributes_class = attributes_class
|
138
|
+
@key = key
|
139
|
+
@serializers = serializers
|
140
|
+
instance_eval(&block) if block_given?
|
141
|
+
end
|
142
|
+
|
143
|
+
def extract_to(data, obj)
|
144
|
+
return if data.nil? || data[key].nil?
|
145
|
+
super(data[key], obj)
|
146
|
+
end
|
147
|
+
|
148
|
+
def insert_to(obj, data)
|
149
|
+
data[key] = {}
|
150
|
+
super(obj, data[key])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/serdee.rb
ADDED
data/serdee.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "serdee/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "serdee"
|
8
|
+
spec.version = Serdee::VERSION
|
9
|
+
spec.authors = ["Ryan Ong"]
|
10
|
+
spec.email = ["ryanong@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Serdee is a serialization/deserialization library for mostly json}
|
13
|
+
spec.description = %q{Serdee is a highly customizable serialization and deserialization library that is strict by default.}
|
14
|
+
spec.homepage = "https://github.com/ryanong/serdee"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
|
24
|
+
spec.add_dependency "activesupport"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: serdee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Ong
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Serdee is a highly customizable serialization and deserialization library
|
84
|
+
that is strict by default.
|
85
|
+
email:
|
86
|
+
- ryanong@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- Gemfile.lock
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- examples/address.rb
|
101
|
+
- examples/request_message.json
|
102
|
+
- examples/request_message.rb
|
103
|
+
- examples/response_code.rb
|
104
|
+
- examples/response_message.json
|
105
|
+
- examples/response_message.rb
|
106
|
+
- examples/unified_message.rb
|
107
|
+
- lib/serdee.rb
|
108
|
+
- lib/serdee/attribute.rb
|
109
|
+
- lib/serdee/attributes.rb
|
110
|
+
- lib/serdee/version.rb
|
111
|
+
- serdee.gemspec
|
112
|
+
homepage: https://github.com/ryanong/serdee
|
113
|
+
licenses: []
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.6.11
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Serdee is a serialization/deserialization library for mostly json
|
135
|
+
test_files: []
|