Skip to content

Commit

Permalink
Raise AWS::DynamoDB::Exception's from Crynamo::Client
Browse files Browse the repository at this point in the history
  • Loading branch information
timkendall committed Dec 30, 2017
1 parent 3daa8bd commit 3dfc48f
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 39 deletions.
8 changes: 4 additions & 4 deletions README.md
Expand Up @@ -43,16 +43,16 @@ Crynamo exposes `Crynamo::Client` as a basic DynamoDB client. The client's API i

```crystal
# Get an item
dynamodb.get("pets", { name: "Doobie" })
dynamodb.get!("pets", { name: "Doobie" })
# Insert an item
dynamodb.put("pets", { name: "Thor", lifespan: 100 })
dynamodb.put!("pets", { name: "Thor", lifespan: 100 })
# Update an item
dynamodb.update("pets", { name: "Thor" }, { lifespan: 50 })
dynamodb.update!("pets", { name: "Thor" }, { lifespan: 50 })
# Remove an item
dynamodb.delete("pets", { name: "Doobie" })
dynamodb.delete!("pets", { name: "Doobie" })
```

## Development
Expand Down
6 changes: 3 additions & 3 deletions examples/simple_client.cr
Expand Up @@ -4,11 +4,11 @@ config = Crynamo::Configuration.new("foo", "secret", "us-east-1", "http://localh
db = Crynamo::Client.new(config)

# Assuming that you have a table named "pets"
db.put("pets", {name: "Thor", age: 100, family_friendly: false})
db.put("pets", {name: "Scooby-Doo", age: 9, family_friendly: true, nickname: "Scooby"})
db.put!("pets", {name: "Thor", age: 100, family_friendly: false})
db.put!("pets", {name: "Scooby-Doo", age: 9, family_friendly: true, nickname: "Scooby"})

# Assuming you have a primary key named "name"
data = db.get("pets", {name: "Scooby-Doo"})
data = db.get!("pets", {name: "Scooby-Doo"})

puts "Found pet:"
puts "Name: #{data["name"]}"
Expand Down
10 changes: 5 additions & 5 deletions spec/crynamo/client_spec.cr
Expand Up @@ -18,7 +18,7 @@ describe Crynamo::Client do
.with(body: "{\"TableName\":\"pets\",\"Key\":{\"name\":{\"S\":\"Scooby\"}}}", headers: {"X-Amz-Target" => "DynamoDB_20120810.GetItem"})
.to_return(status: 200, body: %({"Item":{"lifespan":{"N":"100"},"name":{"S":"Scooby"}}}))

data = client.get("pets", {name: "Scooby"})
data = client.get!("pets", {name: "Scooby"})

data.should eq({
"lifespan" => 100.0,
Expand All @@ -31,7 +31,7 @@ describe Crynamo::Client do
.with(body: "{\"TableName\":\"pets\",\"Key\":{\"name\":{\"S\":\"Missing\"}}}", headers: {"X-Amz-Target" => "DynamoDB_20120810.GetItem"})
.to_return(status: 200, body: "{}")

data = client.get("pets", {name: "Missing"})
data = client.get!("pets", {name: "Missing"})

data.should eq({} of String => JSON::Type)
end
Expand All @@ -46,12 +46,12 @@ describe Crynamo::Client do
.with(body: "{\"TableName\":\"pets\",\"Key\":{\"name\":{\"S\":\"Thor\"}}}", headers: {"X-Amz-Target" => "DynamoDB_20120810.GetItem"})
.to_return(status: 200, body: %({"Item":{"age":{"N":"7"},"family_friendly": {"BOOL": false},"name":{"S":"Thor"}}}))

put_data = client.put("pets", {
put_data = client.put!("pets", {
name: "Thor",
age: 7,
family_friendly: false,
})
get_data = client.get("pets", {name: "Thor"})
get_data = client.get!("pets", {name: "Thor"})

put_data.should eq(nil)

Expand All @@ -70,6 +70,6 @@ describe Crynamo::Client do
headers: {"X-Amz-Target" => "DynamoDB_20120810.DeleteItem"}
).to_return(body: "{}")

client.delete("pets", {name: "Fin"}).should eq(nil)
client.delete!("pets", {name: "Fin"}).should eq(nil)
end
end
16 changes: 16 additions & 0 deletions src/aws/exception.cr
@@ -0,0 +1,16 @@
require "json"

module AWS
# Represents an exception returned from an AWS API
class Exception
JSON.mapping(
__type: String,
message: String,
)

# Get's the human-readable exception type
def type
@__type.split("#").last
end
end
end
59 changes: 32 additions & 27 deletions src/crynamo/client.cr
Expand Up @@ -2,6 +2,7 @@ require "uri"
require "json"
require "http/client"
require "awscr-signer"
require "../aws"

AWS_SERVICE = "dynamodb"

Expand All @@ -25,7 +26,7 @@ module Crynamo
end

# Fetches an item by key
def get(table : String, key : NamedTuple)
def get!(table : String, key : NamedTuple)
marshalled = Crynamo::Marshaller.to_dynamo(key)

query = {
Expand All @@ -34,56 +35,45 @@ module Crynamo
}

result = request("GetItem", query)
error = result[:error]
data = result[:data]

# DynamoDB will return us an empty JSON object if nothing exists
raise Exception.new("Error getting key #{key}") if data.nil?
return {} of String => JSON::Type if !JSON.parse(data).as_h.has_key?("Item")
return {} of String => JSON::Type if !JSON.parse(result).as_h.has_key?("Item")

Crynamo::Marshaller.from_dynamo(JSON.parse(data)["Item"].as_h)
Crynamo::Marshaller.from_dynamo(JSON.parse(result)["Item"].as_h)
end

# Inserts an item
def put(table : String, item : NamedTuple)
def put!(table : String, item : NamedTuple)
marshalled = Crynamo::Marshaller.to_dynamo(item)

query = {
TableName: table,
Item: marshalled,
}

result = request("PutItem", query)

raise Exception.new("Error inserting item #{item}") if result[:error]
# For now just return nil indicating the operation went as expected
# Note: We'll need to solidify an error handling model
nil
request("PutItem", query)
return nil
end

# TODO
def update(table : String, key : NamedTuple, item : NamedTuple)
def update!(table : String, key : NamedTuple, item : NamedTuple)
end

# Deletes an item at the specified key
def delete(table : String, key : NamedTuple)
def delete!(table : String, key : NamedTuple)
marshalled = Crynamo::Marshaller.to_dynamo(key)

query = {
TableName: table,
Key: marshalled,
}

result = request("DeleteItem", query)

raise Exception.new("Error deleting item for key #{key}") if result[:error]
# For now just return nil indicating the operation went as expected
# Note: We'll need to solidify an error handling model
nil
request("DeleteItem", query)
return nil
end

def query(query : NamedTuple)
request("Query", query)
# TODO
def query!(query : NamedTuple)
# request("Query", query)
end

private def request(
Expand All @@ -100,10 +90,25 @@ module Crynamo
status_code = response.status_code
body = response.body

if status_code == 200
{data: body, error: nil}
# Note: Happy path, return what DynamoDB gives us
return body if status_code == 200

# Otherwise construct and AWS::Exception object
exc = AWS::Exception.from_json(body)

# Enumerate all AWS exceptions here
# TODO Use a macro
case exc.type
when "ConditionalCheckFailedException"
raise AWS::DynamoDB::Exceptions::ConditionalCheckFailedException.new(exc.message)
when "ProvisionedThroughputExceededException"
raise AWS::DynamoDB::Exceptions::ProvisionedThroughputExceededException.new(exc.message)
when "ResourceNotFoundException"
raise AWS::DynamoDB::Exceptions::ResourceNotFoundException.new(exc.message)
when "ItemCollectionSizeLimitExceededException"
raise AWS::DynamoDB::Exceptions::ItemCollectionSizeLimitExceededException.new(exc.message)
else
{data: nil, error: body}
raise Exception.new(exc.message)
end
end
end
Expand Down

0 comments on commit 3dfc48f

Please sign in to comment.