<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Richard Forshaw</title>
    <description>The latest articles on DEV Community by Richard Forshaw (@richardforshaw).</description>
    <link>https://dev.to/richardforshaw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1023376%2Fcc4e81c3-fe90-413b-9ac7-662902a3054c.jpeg</url>
      <title>DEV Community: Richard Forshaw</title>
      <link>https://dev.to/richardforshaw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richardforshaw"/>
    <language>en</language>
    <item>
      <title>Serverless Email Processing</title>
      <dc:creator>Richard Forshaw</dc:creator>
      <pubDate>Sat, 29 Apr 2023 03:35:56 +0000</pubDate>
      <link>https://dev.to/richardforshaw/serverless-email-processing-4m86</link>
      <guid>https://dev.to/richardforshaw/serverless-email-processing-4m86</guid>
      <description>&lt;p&gt;There is a lot of information in the emails that fill our inboxes, and it is such a well-known form of communication that it's surprising that it's usually overlooked when providing an interface to a system. In this article, I'll go through how to set up basic lambda processing of emails using SES, Cloudformation and Serverless.&lt;/p&gt;

&lt;h2&gt;
  
  
  SES setup
&lt;/h2&gt;

&lt;p&gt;First we need to set up receiving emails, which is done with SES. In order to capture incoming emails you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An email identity in SES&lt;/li&gt;
&lt;li&gt;An email address and DNS rule&lt;/li&gt;
&lt;li&gt;A rule for what to do with the email when you get it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this section we will go though these.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domains
&lt;/h3&gt;

&lt;p&gt;First a word on domains. To receive email through a 'real' email address you will need a domain. There are two ways to purchase domains in relation to this article: either in AWS Route53 or externally. I have done both and so I will describe both. In some ways external domains are better and in some ways Route53 ones are better. After experiencing both I will probably usually go with external domains but in some cases I will pick Route53. As with many engineering choices, it depends on the needs of the project or product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common SES Setup
&lt;/h3&gt;

&lt;p&gt;To receive and process emails with Amazon SES, you will need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A verified email identity&lt;/li&gt;
&lt;li&gt;A SES rule set&lt;/li&gt;
&lt;li&gt;A SES rule for incoming email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule set and its associated rules can be built with cloudformation but the email identity cannot. In order to do this, go to the AWS SES dashboard and then to the 'Verified Identities' section. From there click on 'Create Identity', to see the following form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2Vhlbjqj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wvttitl0chgt8r0bkw5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2Vhlbjqj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wvttitl0chgt8r0bkw5d.png" alt="Creating an SES Identity" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS have updated their domain verification to only use DKIM, and if you have a domain with Route53 then you can check a box to automatically publish the records to your hostedzone, which is great. This means that you don't have to put anything in to Cloudformation, but it does mean that these changes are not version-controlled in any way, which may or may not suit you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up email with Route53
&lt;/h3&gt;

&lt;p&gt;So you have a Route53 domain. This is great for Infrastructure-as-code as you can specify your configuration in a Cloudformation stack which means it is easy to version-control and re-deploy if necessary.&lt;/p&gt;

&lt;p&gt;Configuring Route53 to handle receiving emails only requires a few fairly simple Cloudformation resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Route53 Hosted Zone&lt;/li&gt;
&lt;li&gt;An incoming email MX record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Cloudformation example of this is below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;MyHostedZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Route53::HostedZone&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HostedZoneConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Domain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;registered&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS"&lt;/span&gt;
      &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mydomain.com"&lt;/span&gt;

  &lt;span class="c1"&gt;# Forward email to us-west for SES processing&lt;/span&gt;
  &lt;span class="na"&gt;IncomingEmailRecord&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Route53::RecordSet&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HostedZoneId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MyHostedZone&lt;/span&gt;
      &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;somefunction.mydomain.com"&lt;/span&gt;
      &lt;span class="na"&gt;Comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Incoming email record for sub-domain&lt;/span&gt;
      &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MX&lt;/span&gt;
      &lt;span class="na"&gt;TTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
      &lt;span class="na"&gt;ResourceRecords&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inbound-smtp.us-west-2.amazonaws.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is at this point that you should be aware of the limitations on SES services. &lt;em&gt;SES is only available in some regions&lt;/em&gt;, and that list of regions is even smaller for handling incoming email. As of writing this article, those regions are as follows and &lt;a href="https://docs.aws.amazon.com/general/latest/gr/ses.html"&gt;are described here&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;US-East-1 (N. Virginia)&lt;/li&gt;
&lt;li&gt;US-West-2 (Oregon)&lt;/li&gt;
&lt;li&gt;EU-West-1 (Ireland)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to receive and process emails, it is best to set up your infrastructure here, however because buckets are global you can probably have lambdas and downstream storage hosted in another region if that is beneficial, but it will make deployment and management more complicated.&lt;/p&gt;

&lt;p&gt;You will see that the &lt;code&gt;RecordSet&lt;/code&gt; item refers to a subdomain. I prefer to do this so that various functions which share processing requirements are grouped under a subdomain. Root domains can be left for general things like sending actual service-related emails.&lt;/p&gt;

&lt;h3&gt;
  
  
  External DNS
&lt;/h3&gt;

&lt;p&gt;If you have an external DNS, you will need to add the DKIM keys by hand. This is straightforward but you should follow any guidance provided by your DNS provider.&lt;/p&gt;

&lt;p&gt;The MX record must also be entered by hand, and is typically a record with the following information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Type&lt;/em&gt;: MX&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Name&lt;/em&gt;: somefunction.mydomain.com&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Priority&lt;/em&gt;: 10&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Target&lt;/em&gt;: inbound-smtp.us-west-2.amazonaws.com&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fields will vary from service to service.&lt;/p&gt;

&lt;p&gt;Once the DKIM and MX records are set up, if they are correct then you should receive an email through your AWS account to say that verification is successful. If not, check the SES dashboard periodically.&lt;/p&gt;

&lt;h3&gt;
  
  
  SES Rules
&lt;/h3&gt;

&lt;p&gt;Now we can move on to the SES rules. This is required to inform AWS what to do with an email that it receives. Setting up a rule requires you to define a rule set and also define rules within that set.&lt;/p&gt;

&lt;p&gt;A common example is to simply store the incoming email so that you can process it later. In CloudFormation this looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;EmailReceiptRuleSet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SES::ReceiptRuleSet&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RuleSetName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-email-rule-set'&lt;/span&gt;

  &lt;span class="na"&gt;EmailRule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SES::ReceiptRule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RuleSetName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;EmailReceiptRuleSet&lt;/span&gt;
      &lt;span class="na"&gt;Rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-incoming-email-storage-rule'&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
        &lt;span class="na"&gt;Recipients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;somefunction.mydomain.com&lt;/span&gt;
        &lt;span class="na"&gt;Actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt;
            &lt;span class="na"&gt;S3Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;EmailStorageBucket&lt;/span&gt;
              &lt;span class="na"&gt;ObjectKeyPrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IncomingEmail/"&lt;/span&gt;
    &lt;span class="na"&gt;DependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EmailBucketDataStoragePolicy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should be straightforward to understand: The ReceiptRuleSet is simply a container, and the actual rule instructs AWS to store any email addressed to &lt;code&gt;somefunction.mydomain.com&lt;/code&gt; to be stored in the referenced storage bucket under the prefix &lt;code&gt;IncomingEmail/&lt;/code&gt;. This acts as a kind of folder in S3.&lt;/p&gt;

&lt;p&gt;You will notice that there is a reference to an &lt;code&gt;EmailBucketDataStoragePolicy&lt;/code&gt;. This is important as you need to allow SES to be able to write to your bucket. This will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;EmailBucketDataStoragePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::BucketPolicy&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;EmailStorageBucket&lt;/span&gt;
      &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt;
            &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AllowSESPutObject"&lt;/span&gt;
            &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow"&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ses.amazonaws.com"&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:PutObject"&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Join&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;EmailStorageBucket.Arn&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Bucket Policy allows SES to perform &lt;code&gt;s3:PutObject&lt;/code&gt; on the referenced bucket. Without this, you will be faced with some hard-to-diagnose permission problems.&lt;/p&gt;

&lt;p&gt;(I haven't included the resource for the actual S3 bucket here - that should be a straightforward exercise for the reader)&lt;/p&gt;

&lt;h4&gt;
  
  
  Gotcha: Enabling the RuleSet
&lt;/h4&gt;

&lt;p&gt;One thing that is a bit hard to find in the documentation is the fact that you need to &lt;strong&gt;manually&lt;/strong&gt; enable the ruleset. You can enable the &lt;strong&gt;rule&lt;/strong&gt; in Cloudformation but the &lt;strong&gt;ruleset&lt;/strong&gt; must be enabled through the console. To do this, go into the 'Email Receiving' menu item, select your ruleset and enable it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JVxBwGYh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k0sns8dwyea73rt01qhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JVxBwGYh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k0sns8dwyea73rt01qhs.png" alt="Activating an SES Ruleset" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda Setup
&lt;/h2&gt;

&lt;p&gt;This is all the infrastructure set up - we now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A domain which sends email to AWS servers&lt;/li&gt;
&lt;li&gt;A bucket to store the emails (set up separately)&lt;/li&gt;
&lt;li&gt;A SES rule which listens to incoming mail and writes it to the bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we need to process it.&lt;/p&gt;

&lt;p&gt;Readers should note that you don't &lt;strong&gt;have to&lt;/strong&gt; write to the S3 bucket first, you can configure the rule to invoke a lambda function straight away, but I find it is useful to store the email anyway. So now we will trigger a lambda from when the email is stored to S3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serverless
&lt;/h3&gt;

&lt;p&gt;I like using the Serverless framework. I just think that it makes things nice and easy and minimises what you need to do to get a lambda up and running.&lt;/p&gt;

&lt;p&gt;We do need to do something though, and the basics are this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up the provider details&lt;/li&gt;
&lt;li&gt;Make sure we have a deployment role&lt;/li&gt;
&lt;li&gt;Give our lambda some permissions to access S3&lt;/li&gt;
&lt;li&gt;Define the function and the trigger event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Item 1 can be done when you set up your Serverless project, and all the info you need is on the &lt;a href="https://www.serverless.com/framework/docs/tutorial"&gt;Serverless tutoral page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Roles and Permissions
&lt;/h3&gt;

&lt;p&gt;Assigning the role and permission is probably the trickiest thing. All the big cloud providers implement some kind of security model, which is a good thing. It's just up to us to use it!&lt;/p&gt;

&lt;p&gt;If you run through the Serverless setup, it will allow you to deploy using your local access keys, but this is not typically recommended. I generally provide a role for Serverless to assume, which restricts what it can and can't do. Setting it up is a bit cumbersome but it means you have full control over the permissions.&lt;/p&gt;

&lt;p&gt;At minimum, it needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be allowed to assume Cloudformation and Lambda roles&lt;/li&gt;
&lt;li&gt;Be allowed to pass a role to another service&lt;/li&gt;
&lt;li&gt;Be allowed to perform certain functions on your resources&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A little explanation of the above points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;is in order for Serverless to actually perform the deployment and create your resources&lt;/li&gt;
&lt;li&gt;is a special case, which will be explained soon&lt;/li&gt;
&lt;li&gt;is so it can perform actions on your resources&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because Serverless prefixes your project's services with the service name, I typically write the IAM PolicyDocuments to be limited to only those resources, which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ServerlessDeploymentRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::IAM::Role&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RoleName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My-Serverless-Deploy-Role&lt;/span&gt;
      &lt;span class="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2012-10-17&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cloudformation.amazonaws.com'&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda.amazonaws.com'&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sts:AssumeRole'&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServerlessDeployPolicies&lt;/span&gt;
          &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2012-10-17&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;   &lt;span class="c1"&gt;# Allow S3 actions to buckets with service prefix.&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:s3:::service-prefix*'&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3:*'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;More permissions...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  A Special Case
&lt;/h4&gt;

&lt;p&gt;I usually make an architectural decision to separate the incoming email infrastructure with the business logic in the lambda code. &lt;em&gt;You don't have to do this&lt;/em&gt;, but I do it for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serverless is not great at handling resources that are not tightly coupled to your lambdas. It does allow you to add custom resources at the end of your &lt;code&gt;serverless.yml&lt;/code&gt; file, but this is typically just CloudFormation code anyway, and brings us to the next point...&lt;/li&gt;
&lt;li&gt;The email pipeline should be a more permanent fixture, and shouldn't be tightly coupled to the serverless deployment in my opinion. If you want to tear down and re-deploy your lambdas, if your email pipeline is managed by the &lt;code&gt;serverless.yml&lt;/code&gt; file then it will be torn down as well. I think it is better to de-couple them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of this, serverless needs to be told that you have a pre-configured S3 bucket and it needs to run some custom lambdas to be able to access it. In order to do this, the following policy needs to be added to the &lt;code&gt;ServerlessDeploymentRole&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;MyServerlessDeploymentRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Existing stuff...&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CustomResourceLambdaRole&lt;/span&gt;
          &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2012-10-17&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;   &lt;span class="c1"&gt;# Allow all IAM actions to roles prefixed with service&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::${AWS::AccountId}:role/My-Serverless-Deploy-Role'&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iam:PassRole'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In essence, the role needs to be allowed to pass itself to another service. This is required so it can install and run a custom lambda to be able to configure your existing S3 bucket. (Once again, if you are happy to let Serverless handle your S3 bucket as well then you won't have to do this.)&lt;/p&gt;

&lt;p&gt;Finally, this role must be named in your new &lt;code&gt;serverless.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
  &lt;span class="c1"&gt;# Your provider configuration...&lt;/span&gt;

  &lt;span class="c1"&gt;# AWS IAM specifications here&lt;/span&gt;
  &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deploymentRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::${aws:accountId}:role/My-Serverless-Deploy-Role&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lambda Permissions
&lt;/h3&gt;

&lt;p&gt;Next we have to grant permissions to the lambda that we are going to create. It will need to be able to read the email that has been stored in the S3 bucket, so we will need to access the ARN of the bucket you are using. In the &lt;code&gt;provider/iam&lt;/code&gt; section of the &lt;code&gt;serverless.yml&lt;/code&gt;, add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# AWS IAM specifications here&lt;/span&gt;
  &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Allow access to S3 objects&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow"&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:ListBucket"&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="c1"&gt;# Put S3 ARN here&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow"&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:GetObject"&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Fn::Join:&lt;/span&gt;
              &lt;span class="s"&gt;- ''&lt;/span&gt;
              &lt;span class="s"&gt;- -&lt;/span&gt; &lt;span class="c1"&gt;# Put S3 ARN here&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/IncomingEmail/*"&lt;/span&gt;   &lt;span class="c1"&gt;# Matches the SES rule action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few ways to reference the S3 ARN. I like to export it from the Cloudformation and then reference it in &lt;code&gt;serverless.yml&lt;/code&gt; using &lt;code&gt;${cf:stack-name.output-name}&lt;/code&gt;, but you can do all kinds of funky stuff with Serverless variables to make sure you don't repeat yourself.&lt;/p&gt;

&lt;p&gt;One important detail is to make sure that the permission you grant to read from matches the location that you configured to write your email to in the SES rule. Once again, there are probably some tricks you can do to make sure these are synchronised.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the function
&lt;/h3&gt;

&lt;p&gt;Finally, we can make our function! The contents of the function is out of scope of this article, but I will say that it is worth reading the &lt;a href="https://docs.python.org/3/library/email.parser.html"&gt;Python Email Parsing Library Documentation&lt;/a&gt;. It undoubtedly contains information you will need if you are writing in Python. (There are NPM email-parsing packages for Javascript, but I like the fact the Python one is built-in).&lt;/p&gt;

&lt;p&gt;The final thing to do is configure the lambda in &lt;code&gt;serverless.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;functions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;email_processor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;handler.process&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Put bucket name here&lt;/span&gt;
        &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3:ObjectCreated:*&lt;/span&gt;
        &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IncomingEmail/&lt;/span&gt;
        &lt;span class="na"&gt;existing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# We are using an existing bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three important things to note here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need to supply the &lt;strong&gt;bucket name&lt;/strong&gt; to the s3 event, NOT the bucket ARN. This usually trips someone up.&lt;/li&gt;
&lt;li&gt;Make sure the S3 object prefix matches the SES rule. (Of course you don't need to use a prefix, but I think it is more informative)&lt;/li&gt;
&lt;li&gt;Because we are using an external S3 bucket, we must specify &lt;code&gt;existing: true&lt;/code&gt;. (Reminder: if you have chosen not to manage your own bucket then you don't need this and serverless will create it for you, BUT tearing down the function will also delete the bucket.)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;It may not seem simple but there is not that much to do in order to capture emails in AWS, and there are many applications. Once capturing and processing is done, SES can be configured to send emails as well. There are probably many applications which can be bootstrapped into a POC using email instead of building and deploying a website, and some users will find interacting through emails much easier anyway.&lt;/p&gt;

&lt;p&gt;So go forth and keep email alive!&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>ChatTDD: Using AI as your Programming Buddy</title>
      <dc:creator>Richard Forshaw</dc:creator>
      <pubDate>Mon, 24 Apr 2023 02:54:46 +0000</pubDate>
      <link>https://dev.to/richardforshaw/chattdd-using-ai-as-your-programming-buddy-4pd1</link>
      <guid>https://dev.to/richardforshaw/chattdd-using-ai-as-your-programming-buddy-4pd1</guid>
      <description>&lt;p&gt;Previously I wrote about transforming an idea into working code using ChatGPT as an assistant. I took an idea and put ChatGPT through a BDD process and an Example Mapping process.&lt;/p&gt;

&lt;p&gt;I was very impressed with the results, but also a bit disappointed from its lack of detail and also lack of determinism. It was not enough to rely on, but I do think it made a useful tool to simply speed this process up.&lt;/p&gt;

&lt;p&gt;What about the second step - transforming an example scenario into code?&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking up from Requirements
&lt;/h2&gt;

&lt;p&gt;In the last post, ChatGPT helped with creating BDD-type scenarios for a simple email parser. Now we will try to convert some of those scenarios into tests and code, using a typical TDD process. We will use one failure case and one success case. The cases are not exactly how ChatGPT gave them to us because as we learnt it was not very good at going down to the detail level. So in between that step and this step there needs to be some manual review and refinement of the cases.&lt;/p&gt;

&lt;p&gt;[In this post we will look at the failure case. The full article has details of implementing a success case.]&lt;/p&gt;

&lt;p&gt;The failure case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with no file attachments returns error&lt;/span&gt;
  &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;no&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;attachments&lt;/span&gt;
  &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;sent&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
  &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;system&lt;/span&gt; &lt;span class="err"&gt;should&lt;/span&gt; &lt;span class="err"&gt;return&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;code&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;"One&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;attachment&lt;/span&gt; &lt;span class="err"&gt;must&lt;/span&gt; &lt;span class="err"&gt;be&lt;/span&gt; &lt;span class="err"&gt;present."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configure ChatGPT
&lt;/h3&gt;

&lt;p&gt;To turn ChatGPT into a developer, I gave it the following system command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an expert python software developer. You know how to understand BDD scenarios of the form:

Scenario: scenario title
Given (an initial context)
When (an event occurs)
Then (a testable outcome is satisfied)

You know TDD and how to write tests first using pytest and pytest-bdd. When writing code, you favour functional programming and avoid side-effects.

You have been asked to assist another python software developer to develop tests and code for a small library to parse email content according to the given BDD specifications.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test 1: Simple failure case
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Writing the test case
&lt;/h3&gt;

&lt;p&gt;I prompted ChatGPT with this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given the following scenario:

&amp;lt;scenario from above&amp;gt;

Write a pytest-bdd test which satisfies this scenario.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and I received the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email_parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_email&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email_content, expected_output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"One image file attachment must be present."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"One image file attachment must be present."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"image.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"One image file attachment must be present."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_email_no_file_attachments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Given an email with no image file attachments
&lt;/span&gt;    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"attachments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# When the email is sent to 'The System'
&lt;/span&gt;    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Then the system should return an error code with the message "One image file attachment must be present."
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not quite the start I was expecting. Some things to note:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It assumed a name for the module and for the function under test, which is fine. We will see if it remembers this&lt;/li&gt;
&lt;li&gt;It assumed an input format to the function under test as a single json structure&lt;/li&gt;
&lt;li&gt;It assumed an output from the function under test as a tuple.&lt;/li&gt;
&lt;li&gt;It has not used the pytest-bdd library as we requested in the system message&lt;/li&gt;
&lt;li&gt;The parameterization of the test is not really necessary. The test really only needs one example of having no attachments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second item is interesting, and I like it. The design that initially came into my head was that the function in question would parse the entire email message and handle the extraction of body text and attachments. When I initially saw the output from ChatGPT I was annoyed but then it made me think: this is actually a better solution. Perhaps it was even following my direction to program in a functional way?&lt;/p&gt;

&lt;p&gt;I like writing things in a functional way. Typically it is good to write simple functions that do only one thing with no side-effects, which means they are easier to tests. In the long run I probably would have refactored the code to do this, but the ChatGPT output made me take this decision earlier, and I changed the design in my head to first parse the email into its sub-parts and then process the parts according to my requirements.&lt;/p&gt;

&lt;p&gt;To fix the other issues, I instructed ChatGPT to refine the solution, by asking it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the pytest-bdd library format, providing it with a scenario file name&lt;/li&gt;
&lt;li&gt;return a json structure instead of a tuple&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The updated test function was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pytest_bdd&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email_parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_email&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'../features/email-parsing.feature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Email with no file attachments returns error'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_email_no_file_attachments&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"an email with no image file attachments"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email_with_no_file_attachments&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"attachments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the email is sent to 'The System'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email_to_system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_with_no_file_attachments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parse_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;email_with_no_file_attachments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the system should return an error code with the message &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;One image file attachment must be present.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_error_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_email_to_system&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"One image file attachment must be present."&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;send_email_to_system&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like a pretty good bdd-style test to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--N5_aYnq4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dw5ksfyggh68hsc33hw5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--N5_aYnq4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dw5ksfyggh68hsc33hw5.jpg" alt="Writing Code" width="640" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Code
&lt;/h3&gt;

&lt;p&gt;When I ran the code, I got the usual errors, as you would expect from real TDD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No module&lt;/li&gt;
&lt;li&gt;No function&lt;/li&gt;
&lt;li&gt;No scenario file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I fixed these up and then asked ChatGPT to write the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now write the code which satisfies the test function.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response was this (line-splitting done by me):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if there are any image file attachments
&lt;/span&gt;    &lt;span class="n"&gt;has_image_attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attachment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".jpg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".jpeg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".png"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;has_image_attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="c1"&gt;# If no image file attachments are present, return an error message
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_image_attachment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"One image file attachment must be present."&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# If one or more image file attachments are present, return a success message
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Email parsed successfully."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting thing for this first implementation is that because we had been talking about image attachments, it gave me code which already handled different types of image extensions. This was good but it is against the TDD philosophy of writing just enough code to pass the test. I decided to leave it in however.&lt;/p&gt;

&lt;p&gt;Personally I don't find this code readable, and for the future it is likely the code will accept some extensions and not others, so we can ask ChatGPT to refactor it to use the &lt;code&gt;split&lt;/code&gt; command and the &lt;code&gt;in&lt;/code&gt; operator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the test
&lt;/h3&gt;

&lt;p&gt;The moment of truth... does this work? I ran &lt;code&gt;pytest&lt;/code&gt; and got the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;E       fixture &lt;span class="s1"&gt;'email_with_no_file_attachments'&lt;/span&gt; not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmmm... why? Well, it turns out that the test code generated by ChatGPT had used the pytest 'fixture' style to write the tests, but it had been implemented incorrectly. It's possible that this style is too new for ChatGPT or it just didn't know how to do it. But it is something I had to dig into and fix, which took about an hour.&lt;/p&gt;

&lt;p&gt;After fixing that, hey presto, test passed! The question remaining is: considering the time it took to fix the error, would it have been faster for me to write this myself?&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This was a valuable experiment with many new things learnt about using ChatGPT in this way.&lt;/p&gt;

&lt;p&gt;The main benefit I found was that it prompted me to think more about design decisions - it did almost feel like I was 'pair programming'. However I was hoping that I would be able to continue thinking at a high level, but that didn't happen; because some of the output had mistakes or was poorly written (in my opinion), I had to jump between design-level and code-level thinking.&lt;/p&gt;

&lt;p&gt;This raised an interesting question though - does this in fact fit into the TDD 'Red-Green-Refactor' process? Maybe it does and I have just been executing the 'refactor' stage a bit too early? I admit I sometimes neglected to wait to see if the code that was produced passed, thus following the 'write the minimum code to turn the test green' rule.&lt;/p&gt;

&lt;p&gt;Other associated points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defining the expected inputs/outputs for unit tests is a bit laborious&lt;/li&gt;
&lt;li&gt;Some responses introduced assumptions in the details (e.g. I asked it to use URL-encoded-strings and it used outdated formats which I had to fix)&lt;/li&gt;
&lt;li&gt;The output is often verbose, leading to lengthy, duplicated or redundant code. This was especially true when parameterizing tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these may be improved in GPT4, we will have to see.&lt;/p&gt;

&lt;p&gt;What about writing the more complex success-case of the experiment? You can read that at &lt;a href="http://www.developdeploydeliver.com/blog/articles/2023-04-ChatTDD-pair-programming-with-ai/"&gt;the original post&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>development</category>
      <category>tdd</category>
    </item>
    <item>
      <title>ChatBDD: Using AI in a BDD Process</title>
      <dc:creator>Richard Forshaw</dc:creator>
      <pubDate>Wed, 12 Apr 2023 01:33:47 +0000</pubDate>
      <link>https://dev.to/richardforshaw/chatbdd-using-ai-in-a-bdd-process-1h6h</link>
      <guid>https://dev.to/richardforshaw/chatbdd-using-ai-in-a-bdd-process-1h6h</guid>
      <description>&lt;p&gt;In embarking on a new side-project, I was faced with writing code again. I love writing code, but it certainly takes mental effort. I have noticed that writing entire modules on my own requires me to think about multiple things and draw an many areas of experience, from test design and using the core language libraries all the way up to trying to keep the overall goal and high-level architecture in mind. And at the same time trying to keep things simple.&lt;/p&gt;

&lt;p&gt;Usually in this situation I would simply start coding (tests first of course), but sometimes I would fail to stick to a clear coherent path, instead getting lost in a sea of ideas due to my excitement in how amazing the new system would be. So could I use ChatGPT to help me focus, while using a BDD or TDD process?&lt;/p&gt;

&lt;p&gt;Let's find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Method: From behaviour to code
&lt;/h2&gt;

&lt;p&gt;I decided to take this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the intended outcome of my module in order to write scenarios&lt;/li&gt;
&lt;li&gt;Use each scenario to turn into tests&lt;/li&gt;
&lt;li&gt;Use the tests to write the code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I believe there is a huge benefit of using ChatGPT to do this, especially point 3, because as humans sometimes we can't help but be creative, but TDD dictates that once you write the test you should write the code to make it pass and ONLY enough code to make it pass. Thus using ChatGPT makes it easier to stick within this pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the outcomes
&lt;/h2&gt;

&lt;p&gt;When I was consulting, there were two key outcomes that I looked for when preparing for a sprint: defining the general desired behaviour and providing specific examples. This typically meets a 'definition of done', in that the general behaviour of the system is described and there are some testable examples which allow the developers to ensure that the code that they have written meets some specific testable criteria.&lt;/p&gt;

&lt;p&gt;I started off asking ChatGPT this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an expert business analyst with additional expertise in Software User Stories and BDD. You help turn business requirements given as User Stories into multiple BDD Given-When-Then scenarios.

A User Story is of the format:
As a (user type)
I want to (perform an action on the system)
So that (I meet a desired outcome)

A Given-When-Then scenario is of the format:
Given (an initial context)
When (an event occurs)
Then (a testable outcome is satisfied)

---

I am writing a software module which accepts an email from a user and extracts the components so that they can be used to post to Instagram. The user story is:

As an Instagram user,
I want to send an email to 'The System' which contains an image and some text
So that the image and text are extracted in a way that it can be posted to Instagram

Write BDD scenarios which meet the user story.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To my amazement, ChatGPT output the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;1.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with image and text is successfully processed&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;some&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;extracted&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;Instagram&lt;/span&gt; &lt;span class="err"&gt;posting&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt;

&lt;span class="err"&gt;2.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with no image is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;no&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;3.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with no text is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;no&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;4.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with an image that cannot be processed is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;that&lt;/span&gt; &lt;span class="err"&gt;cannot&lt;/span&gt; &lt;span class="err"&gt;be&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;5.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with text that exceeds the character limit is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;that&lt;/span&gt; &lt;span class="err"&gt;exceeds&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;character&lt;/span&gt; &lt;span class="err"&gt;limit&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;6.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with an image that exceeds the size limit is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;that&lt;/span&gt; &lt;span class="err"&gt;exceeds&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;size&lt;/span&gt; &lt;span class="err"&gt;limit&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;7.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with unsupported image format is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;unsupported&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;format&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;8.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email is received from an unauthorized email address&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;unauthorized&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;address&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;9.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generated Instagram post is successfully saved to file&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;Instagram&lt;/span&gt; &lt;span class="err"&gt;posting&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;successfully&lt;/span&gt;

&lt;span class="err"&gt;10.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generated Instagram post fails to save to file&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;Instagram&lt;/span&gt; &lt;span class="err"&gt;posting&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;formatted&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;successfully&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What a start! I am usually able to come up with reasonable failure scenarios, and the first thing I noticed was there were scenarios here which I hadn't even considered yet, for example the character limits and attachment size limits.&lt;/p&gt;

&lt;p&gt;The fact that this was generated in about a minute should not be overlooked. I estimate that this would take me about half and hour to think of an type up. So we are looking at an incredible speed-up.&lt;/p&gt;

&lt;p&gt;Examining the output, there were a few things that for an MVP I did not consider important since I would be the only user (un-authorised email addresses and image format). There were also some things which were missing, probably due to some vagueness in my starting story. So I asked for some refinements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please make the following changes:

 1. The image is received as an attachment and only one attachment is allowed.
 2. The outcome is not a formatted image. The outcome is that the image is extracted and saved, and the text is extracted and saved in a suitable format for subsequent processing.
 3. The text in the email can specify multiple image captions and a carousel caption.
 4. The product is an MVP and so scenarios 6, 7 and 8 are not required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I will spare the duplication, so here are the updated scenarios that it created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;1.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with one image attachment and text is successfully processed&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;one&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;attachment&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;extracted&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;suitable&lt;/span&gt; &lt;span class="err"&gt;format&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;subsequent&lt;/span&gt; &lt;span class="err"&gt;processing&lt;/span&gt;

&lt;span class="err"&gt;2.&lt;/span&gt; &lt;span class="err"&gt;[unchanged]&lt;/span&gt;

&lt;span class="err"&gt;3.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with more than one image attachment is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;more&lt;/span&gt; &lt;span class="err"&gt;than&lt;/span&gt; &lt;span class="err"&gt;one&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;attachment&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;processed&lt;/span&gt; &lt;span class="err"&gt;further&lt;/span&gt;

&lt;span class="err"&gt;4.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email with text specifying multiple image captions and a carousel caption is received&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;contains&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;specifying&lt;/span&gt; &lt;span class="err"&gt;multiple&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;captions&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;carousel&lt;/span&gt; &lt;span class="err"&gt;caption&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;received&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;'The&lt;/span&gt; &lt;span class="err"&gt;System'&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;captions&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;carousel&lt;/span&gt; &lt;span class="err"&gt;caption&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;extracted&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;captions&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;suitable&lt;/span&gt; &lt;span class="err"&gt;format&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;subsequent&lt;/span&gt; &lt;span class="err"&gt;processing&lt;/span&gt;

&lt;span class="err"&gt;5.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extracted image and text are successfully saved&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;extracted&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;successfully&lt;/span&gt;

&lt;span class="err"&gt;6.&lt;/span&gt; &lt;span class="py"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extracted image and text fail to save&lt;/span&gt;
   &lt;span class="err"&gt;Given&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;extracted&lt;/span&gt; &lt;span class="err"&gt;from&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;email&lt;/span&gt;
   &lt;span class="err"&gt;When&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt;
   &lt;span class="err"&gt;Then&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;error&lt;/span&gt; &lt;span class="err"&gt;message&lt;/span&gt; &lt;span class="err"&gt;is&lt;/span&gt; &lt;span class="err"&gt;returned&lt;/span&gt;
   &lt;span class="err"&gt;And&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;image&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;text&lt;/span&gt; &lt;span class="err"&gt;are&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;saved&lt;/span&gt; &lt;span class="err"&gt;successfully&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm... ok, so it removed 6, 7 and 8 as requested, but it also removed scenario 3, 4 and 5 which was weird. This then drew my attention to the fact that scenarios 5 and 6 were not really saying much. I decided to keep scenario 6 because maybe there would be an internal fault which is a fair test case, but scenario 5 is basically the same as scenario 1.&lt;/p&gt;

&lt;p&gt;I have found that directing existing output from ChatGPT can be tricky and frustrating, but I could still hand-pick the best of all the outputs. However these scenarios are still quite high-level - what about that missing detail? I decided to try another approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refinement: Example Mapping
&lt;/h2&gt;

&lt;p&gt;When I was introduced to Example Mapping I loved it. As a developer, if given a detailed user story or requirement, I always found myself thinking of examples to test and often found that I was unable to think of them or that they were incorrect because of a misunderstanding.&lt;/p&gt;

&lt;p&gt;Lack of examples could be fixed mid-sprint by talking to a stakeholder, but incorrect scenarios would not usually be found until the end of the sprint. Example Mapping seeks to change this, and involves stakeholders, developers and QA discussing the scenarios to define examples of what they mean.&lt;/p&gt;

&lt;p&gt;The scenarios from the first experiment may already look complete, but they use language such as 'an error message is returned' which is not specific or testable. In order to get more detail from ChatGPT, I decided to put it in an Example Mapping scenario instead. Since I could wear at least 2 of the caps from an Example Mapping meeting, I could direct ChatGPT to write some examples. I used the &lt;a href="https://dev.tolink"&gt;rules for Example Mapping&lt;/a&gt; together with the rules for BDD scenarios and pushed through the user story with some more request details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a software product specialist with knowledge of BDD, User Stories and Example Mapping. You are participating in an Example Mapping session. This session follows these rules for a given user story:

Start by writing the story under discussion on a yellow card.

Next write each of the acceptance criteria, or rules that we already know, on a blue card.

For each rule, we may need one or more examples to illustrate it. We write those on a green card and place them under the relevant rule.

Examples are written in the BDD Given-When-Then scenario format as follows:
Given (an initial context)
When (an event occurs)
Then (a testable outcome is satisfied)

As we discuss these examples, there may be questions that nobody in the room can answer. Capture those on a red card and move on with the conversation.

---

Take the following user story:

As an Instagram user,
I want to send an email to 'The System' which contains an image and some structured text
So that the image and text are extracted in a way that it can be posted as an Instagram post or carousel

Take the following rules:

1. The email must contain 1 image file attachment
2. The email must have a subject "INSTAGRAM POST &amp;lt;date&amp;gt;"
3. The email must contain some body text
4. The body text must be structured to create either a single image post or a carousel post of 2 or 3 images
5. The structured body text must contain a general post caption and a caption for each desired image in the post
6. Error messages are returned as a HTTP 400 code with an associated message

Write the examples as per an Example Mapping session, and also write any unknown questions.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I won't bore you with the text output - instead lets put them onto cards as they would be in a real Example Mapping session. Unfortunately image generators are not good at producing text (yet), so this one is a manual effort - please excuse any poor formatting as some output was quite long.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jnhxNLJW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u3fzy8dwvro35yie1gft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jnhxNLJW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u3fzy8dwvro35yie1gft.png" alt="Example Mapping result" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this looks pretty good, and once again it only took about a minute to produce it. However there are some issues, and in a real session I wouldn't let this out of the room. Let's look at the positives first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT correctly structured the cards (in the text output the examples were correctly aligned under the rules)&lt;/li&gt;
&lt;li&gt;ChatGPT did make examples according to the rules, and distinguished between input scenarios&lt;/li&gt;
&lt;li&gt;The additional questions blew me away - they are all valid: attachment naming, additional body text and receipt address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the negative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user story outcome has again been interpreted as posting to Instagram, when it is not&lt;/li&gt;
&lt;li&gt;The failure scenarios are testable but the success scenarios are not really testable. I prefer examples describing outcomes which can be translated almost directly into a test.&lt;/li&gt;
&lt;li&gt;The carousel scenario is missing a test for 3 captions&lt;/li&gt;
&lt;li&gt;The carousel scenario is missing a failure case for more than 3 captions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, you can say that these are because it has not been given sufficient direction (again). In the first two points this is perhaps true, but in the final two I think there is enough information for a human to have understood (like the rule for images). But this is often what is discussed in Example Mapping meetings.&lt;/p&gt;

&lt;p&gt;Since this is an experiment with ChatGPT, let's try again with more direction, and answer some of the questions. Here is the updated request and rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Take the following user story:

As an Instagram user,
I want to send an email to 'The System' which contains an image and some structured text
So that the image and text are extracted and stored so that it can be later used to generate an Instagram post.

Take the following rules:

1. The email is addressed to "me@postrobot.com"
2. The email must contain 1 image file attachment and can have any name
3. The email must have a subject "INSTAGRAM POST &amp;lt;date&amp;gt;"
4. The email must contain some body text
5. Body text containing "POST TEXT" and "CAPTION" labels create a single image post
6. Body text containing "POST TEXT" and "CAPTION-1"..."CAPTION-N" labels create a carousel post
7. A carousel post contains no more than 3 images
8. Text extracted from the email is stored as JSON, containing the date, post text, captions and image name
9. Error messages are returned as a HTTP 400 code with an associated message

Write the examples as per an Example Mapping session, and also write any unknown questions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For brevity, I will spare the detailed output, but below are the key results from the response:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The output now only contained a positive example scenarios, whereas previously it also contained negative ones&lt;/li&gt;
&lt;li&gt;Because there were no negative scenarios, the only output involving an error (responding to (9)) was "Then a HTTP 400 code with the associated message is returned", which was not helpful&lt;/li&gt;
&lt;li&gt;Some of the questions it now raised had already been deduced in the previous attempt, e.g. now it raised the question: "What happens if there is no image attachment in the email?". I think a human would have deduced this from rules (2) and (9)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bUk7HEfg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sbvkphij38fl181a6b2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bUk7HEfg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sbvkphij38fl181a6b2.jpg" alt="Hitting The Target" width="550" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Partnering with ChatGPT to expand a user story into testable scenarios once again showed how a LLM such as this is very useful from a general stand-point but not from a detailed one. On the positive side, all the answers above were obtained in seconds, meaning I spent more time analysing the results and looking for errors than typing the prompts and waiting for output.&lt;/p&gt;

&lt;p&gt;All in all I probably only spent a few minutes actually interacting with the system. It gave impressive results to allow me to build up a list of scenarios to consider, and also questions that I might have missed. So in a sense, it acted very well as a team-mate.&lt;/p&gt;

&lt;p&gt;The problems exist when searching for detail. Perhaps I am picky, but some of the output was not useful to move forward with. Using ChatGPT for high-level behaviour was good, but any thought of straying down to any level of detail left me disappointed. If it is common to re-prompt with more detail, then I might as well just add the detail myself.&lt;/p&gt;

&lt;p&gt;I also found a lack of consistency between prompts. It mostly performed well but it forgot previous outcomes enough to frustrate me. If I saw this behaviour in a software developer working on my team, it would annoy me.&lt;/p&gt;

&lt;p&gt;Perhaps this is expected - after all LLMs do not 'think', they simply perform highly-advanced statistical pattern-matching. For many purposes this is sufficient and provides plenty of useful responses and food for thought. But at the moment it takes a human to go that step further. (Note that this was done on ChatGPT3.5. Perhaps GPT4 goes further).&lt;/p&gt;

&lt;p&gt;Overall, ChatGPT gets top marks as a productivity assistant. I suspect that the swift generation of output, with a little extra consideration on the inputs, would save a lot of time in the real world, if not simply for automating the output of boilerplate text. So I can see a huge benefit.&lt;/p&gt;

&lt;p&gt;This was originally posted &lt;a href="http://www.developdeploydeliver.com/blog/articles/2023-04-ChatBDD-from-ideas-to-code/"&gt;on my blog&lt;/a&gt;. To see part 2, head on over!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>chatgpt</category>
      <category>software</category>
    </item>
    <item>
      <title>3 Kent Beck Software Insights</title>
      <dc:creator>Richard Forshaw</dc:creator>
      <pubDate>Wed, 08 Mar 2023 03:08:33 +0000</pubDate>
      <link>https://dev.to/richardforshaw/3-kent-beck-software-insights-4fb6</link>
      <guid>https://dev.to/richardforshaw/3-kent-beck-software-insights-4fb6</guid>
      <description>&lt;p&gt;I recently read Kent Beck's book 'Implementation Patterns'. While most of it is focused on Java and OO-programming, there is still much that he talks about which is relevant to programing and software development as a whole.&lt;/p&gt;

&lt;p&gt;Programming is hard, and developing large-scale software long-lived software is harder still as it involves disciplines and behaviours that aren't taught in most programming courses, or at least they weren't when I was at University. Perhaps now there is more importance given to them, but in case there isn't here are three things from Kent's book which I think every developer needs to consider.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cost
&lt;/h2&gt;

&lt;p&gt;When you step out of a programming course and into a software development team, you are entering a much different world than the one that you left. The one you left was concerned with how to tell a computer to solve a certain problem for you, whereas the new one adds time and cost constraints into the mix, together with the dreaded 'L'-word (Legacy), and nobody really prepares you for that.&lt;/p&gt;

&lt;p&gt;Kent dedicates a whole chapter to software cost; a short chapter but a chapter non-the-less. As with any business department or function, cost will eventually become a driving force behind its operation. This is of course still true today but still not fully understood by many developers.&lt;/p&gt;

&lt;p&gt;Costs come in many forms, and Kent cites &lt;a href="https://archive.org/details/Structured_Design_Edward_Yourdon_Larry_Constantine/mode/2up"&gt;Yourdon and Constantine&lt;/a&gt;, saying that the cost of maintaining a software system is usually greater than the cost of developing it (in this case the book defines maintenance as including bug-fixing and modification). This is most likely still true today even with modern engineering techniques.&lt;/p&gt;

&lt;p&gt;As Kent says under his laws of programs: "There is no such thing as 'done'". These days we tend to be aware of this fact, but he breaks this maintenance cost down even further, saying that it it is made up of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost of understanding&lt;/li&gt;
&lt;li&gt;Cost of changing&lt;/li&gt;
&lt;li&gt;Cost of testing&lt;/li&gt;
&lt;li&gt;Cost of deploying&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the first of these, Kent says "Learning what the current code does is the expensive part". This is certainly true today, because software engineering is still knowledge-work, and knowledge-work requires a depth of understanding prior to execution. Sadly much of this understanding is still lost when transferring a system, module or even function between developers and my career has seen many and continued attempts to fix this.&lt;/p&gt;

&lt;p&gt;It is worth understanding the impact of these costs but you should also recognise that the costs are just a symptom - an indicator (albeit a critical one) that allows us to see where there may be another problem lurking.&lt;/p&gt;

&lt;p&gt;Two approaches to lower these costs that may be elicited from Kent's book are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fix the cost of understanding by improving communication; and&lt;/li&gt;
&lt;li&gt;consider that costs can be reduced if there were less things to change or overhead to change them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will look at these both below.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Communication and Naming
&lt;/h2&gt;

&lt;p&gt;Communication is one of the most underrated properties of software programs, but one of the most dualistic. I wager there is not a developer reading this who has been frustrated not being able to understand another developer's code as quickly as they wanted. But by inference, it means that the code they have written has caused the same frustration. As demonstration, I willingly put my hand up and admit that I have re-visited code I have written within 12 months and been initially confused as to what it is doing.&lt;/p&gt;

&lt;p&gt;One of my favourite quotes from Donald Knuth is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The best programs are written so that computing machines can perform them quickly and so that human beings can understand them clearly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And personally, I would not lose too much sleep if the "computing machines" do not perform them quickly, since not all programs need to run fast but &lt;em&gt;they all need to be read by other programmers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The biggest influence we have over a computer program, aside from how it is designed, is the words that we choose when writing it. Kent and I share the values that communication, and by extension, naming variables, functions and classes are among the most important considerations and deserve thought.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Finding just the right name is one of the most satisfying moments in programming"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is good that your algorithm keeps thousands of messages running through your system, but if the next developer to maintain your code doesn't realise its function easily then it is likely to spell trouble.&lt;/p&gt;

&lt;p&gt;Kent's advice is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Convey purpose, type and lifetime of the variable to readers"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I admit that even I learnt something when reading this as I would rarely consider considering the lifetime in this equation, but it is important and often indicates the relevance and importance of a variable within the code.&lt;/p&gt;

&lt;p&gt;A worthy example of appropriate naming is in Kent's description of an "Explaining Message". This is defined as a message (i.e. a method on an object) which distinguishes between its intention and its implementation. Specifically, Kent recalls when he saw a method defined like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;highlight(Rectangle area) {
    reverse(area);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Why is this useful?" he asked himself... why not just expose &lt;code&gt;reverse()&lt;/code&gt; on the object. But this displays exactly what is meant between &lt;em&gt;intention&lt;/em&gt; and &lt;em&gt;implementation&lt;/em&gt;. The intent (i.e. you could say the API action that is useful to the user) is to &lt;em&gt;highlight&lt;/em&gt;. Internally however, it is implemented as reversing the area of the given shape. Subtle but very useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Change and Trade-offs
&lt;/h2&gt;

&lt;p&gt;The other way of looking at reducing the cost of maintenance is to reduce the likelihood of change that will be required. If you incur a cost per change then you can either reduce the cost (as above) or reduce the number of changes. This however is a slippery slope.&lt;/p&gt;

&lt;p&gt;In saying this, we are advocating for introducing &lt;em&gt;flexibility&lt;/em&gt; into the system, but we must recognise there are two types of flexibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one driven by the actual requirements&lt;/li&gt;
&lt;li&gt;one driven by second-guessing future requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kent points out that flexibility comes with a cost, and usually introduces complexity which may be unnecessary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Programs should be flexible but only in ways they change. If [something] never changes, all that complexity is cost without benefit."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This shares much with another of Donald Knuth's famous quotes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Premature optimization is the root of all evil."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kent uses a feature of OO to demonstrate this in the section about behavioural messages. One beautiful thing about OO programming is polymorphism, a feature allowing conditional decisions to be made at run-time in an elegant way instead of what he calls 'explicit conditionals' (i.e the control-flow statements built-in to the language). Explicit conditionals are easy to read but provide little flexibility when it comes to extending behaviour. Polymorphism provides a method to control the execution flow in a more flexible way which does not require existing code to be changed (basically the &lt;a href="https://www.oodesign.com/open-close-principle"&gt;Open-Closed Principle&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Kent however recognises that while this makes code more flexible, it requires more skill to read, understand and write, and introduces problems in communicating to other programmers. Thus it is a trade-off in the change domain.&lt;/p&gt;

&lt;p&gt;The association of flexibility with complexity is one of programming's many great trade-offs, and because of this flexibility should be introduced judiciously. One approach to this is by doing agile development (and doing it correctly). Because agile (e.g. Scrum) involves releasing working software often, you must make the flexibility vs simplicity decision early and within your delivery cadence constraints. I suspect that this will typically come down on the 'simplicity' side, and thus validate Kent's statement that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The time to introduce flexibility is when it is definitely needed"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of over-complicating with too much flexibility, Kent advocates for choosing simplicity and effective testing. I am a big proponent of TDD and automated testing and there have been many occasions where an effective suite of tests have given me confidence in being able to change code implementation safely and quickly. But only when it is needed.&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Turbocharge Your AWS CLI Skills with JSON Parsing</title>
      <dc:creator>Richard Forshaw</dc:creator>
      <pubDate>Thu, 09 Feb 2023 01:42:41 +0000</pubDate>
      <link>https://dev.to/richardforshaw/turbocharge-your-aws-cli-skills-with-json-parsing-g95</link>
      <guid>https://dev.to/richardforshaw/turbocharge-your-aws-cli-skills-with-json-parsing-g95</guid>
      <description>&lt;p&gt;I am a self-professed want-to-be keyboard-wizard, and I was brought up on amazing text-manipulation tools like grep, sed and awk. Text-based data manipulation can still be very fast and powerful and might save you dozens of lines of code. This post shows how a little knowledge of JSON-parsing tools can go a long way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;The tools used in this article are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI&lt;/li&gt;
&lt;li&gt;JMES Path parser&lt;/li&gt;
&lt;li&gt;JQ&lt;/li&gt;
&lt;li&gt;Bash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination may mean that some examples below may not map exactly to your particular use-case or configuration, so please bear that in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic JSON Formatting and Filtering
&lt;/h2&gt;

&lt;p&gt;I looked at basic formatting and filtering in this post about &lt;a href="http://www.developdeploydeliver.com/blog/articles/2022-08-30-aws-cli-essentials/" rel="noopener noreferrer"&gt;essential AWS CLI skills&lt;/a&gt;. As a quick recap, you can use the &lt;code&gt;--query&lt;/code&gt; command-line parameter to pass a string specifying a query expression on the JSON results, and sometimes these can get quite complex.&lt;/p&gt;

&lt;p&gt;If you want a bit more power in your querying, it is worth looking at the &lt;a href="https://stedolan.github.io/jq/manual/" rel="noopener noreferrer"&gt;JQ tool&lt;/a&gt;, which also lets you process JSON structures. The good thing about using JQ is that if can operate on files as well as STDIN, so you can save your JSON output into a file and run JQ over and over. This will definitely be faster when writing your queries and may also save you some AWS processing cost.&lt;/p&gt;

&lt;p&gt;As a start, JQ is great for simply pretty-formatting JSON output from something like a lambda function, by just piping it into &lt;code&gt;jq '.'&lt;/code&gt;. This isn't necessary with the AWS CLI because it already formats the JSON output.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS CLI examples
&lt;/h3&gt;

&lt;p&gt;The simplest use of the CLI filter is to print a reduced amount of data so that it is more manageable. This uses &lt;a href="https://jmespath.org/tutorial.html" rel="noopener noreferrer"&gt;JMES Path expressions&lt;/a&gt; to process the JSON data in the output.&lt;/p&gt;

&lt;p&gt;Here are some basic examples. Note that the query expression is added using the &lt;code&gt;--query&lt;/code&gt; argument. In this case we will use CloudFormation output:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws cloudformation describe-stacks --query "&amp;lt;filter-goes-here&amp;gt;"&lt;/code&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show only stack ID, name and update time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'Stacks[*].[StackId, StackName, LastUpdatedTime]'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show stack name/ID for stacks whose name contains 'foo'&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'Stacks[?StackId.contains(@, 'foo')].[StackId, StackName]'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show stack name/ID for stacks which have outputs exported &lt;em&gt;and&lt;/em&gt; have been updated since Nov 2022&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Stacks[?Outputs &amp;amp;&amp;amp; LastUpdatedTime&amp;gt;'2022-11'].[StackId,StackName,LastUpdatedTime]"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Note the final example only works because the date format in &lt;code&gt;LastUpdatedTime&lt;/code&gt; is able to be compared as a string - more on that later.)&lt;/p&gt;

&lt;h3&gt;
  
  
  JQ
&lt;/h3&gt;

&lt;p&gt;If you want to do serious local-processing of AWS or any other JSON output, you need to get familiar with &lt;a href="https://stedolan.github.io/jq/manual/" rel="noopener noreferrer"&gt;JQ&lt;/a&gt;, an awesome tool which lets you process JSON structures, not only filtering it like the AWS CLI, but also restructuring it.&lt;/p&gt;

&lt;p&gt;Here are a few basic ways to use it. Note that you can either pipe input into JQ or provide a filename which contains your JSON.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simply pretty-print JSON output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jq '.'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get all the sort keys from a dynamo query response &lt;br&gt;(assuming they are strings)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jq '.Items[].SortKey.S'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Put the result into a list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jq '[.Items[].SortKey.S]'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only print keys, not values&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jq 'keys[]'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  An Important Note
&lt;/h3&gt;

&lt;p&gt;It should be stressed here that &lt;strong&gt;QUOTING IS IMPORTANT&lt;/strong&gt;. You may have noticed above that for the AWS &lt;code&gt;--query&lt;/code&gt; I used double-quotes to enclose the whole expressions, and single-quotes when quoting literals within the expression. But for JQ, you use the opposite, which is made clear &lt;a href="https://stedolan.github.io/jq/manual/#Invokingjq" rel="noopener noreferrer"&gt;in the manual&lt;/a&gt;. At least within the environment I am using (bash), &lt;em&gt;not following this will only lead to endless miserable debugging&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing The Two
&lt;/h2&gt;

&lt;p&gt;In general, the &lt;code&gt;--query&lt;/code&gt; filters using JMES are a little more concise than their JQ alternatives, but in my opinion the sequential nature of JQ using pipes (&lt;code&gt;|&lt;/code&gt;) is more readable than JMES. However, there are some other important differences to consider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CLI --query expression&lt;/th&gt;
&lt;th&gt;JQ Expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Usage&lt;/td&gt;
&lt;td&gt;Only with AWS CLI commands&lt;/td&gt;
&lt;td&gt;With any output or file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Only outputs filtered results&lt;/td&gt;
&lt;td&gt;Can restructure into new JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Types&lt;/td&gt;
&lt;td&gt;Does not handle dates natively&lt;/td&gt;
&lt;td&gt;Handles date conversions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripting&lt;/td&gt;
&lt;td&gt;Expression must be entered on command-line&lt;/td&gt;
&lt;td&gt;Expression can be store in file with comments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Below are some common queries I've used, with the CLI query and JQ query side-by-side. You should note that all the JQ expressions are wrapped in &lt;code&gt;[]&lt;/code&gt;, because by default JQ does not output a list. The AWS CLI query function does output a list, so the additional &lt;code&gt;[]&lt;/code&gt; are used to match the outputs. For these examples, I am using DynamoDB output which looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Items": [
        {
            "PartitionKey": { "S": "blog/books/2023-01-drive-daniel-pink/" },
            "SortKey": { "S": "1675071762" },
            "SomeField": { "N": "54" },
            // Other Fields...
        },
        {
            "PartitionKey": { "S": "blog/books/2023-01-drive-daniel-pink/" },
            "SortKey": { "S": "1675071862" },
            "SomeField": { "N": "44" },
            // Other Fields...
        },
        // More items...
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;AWS CLI query expression&lt;/th&gt;
&lt;th&gt;JQ Expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show all sort keys from dynamo output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Items[*].SortKey.S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[].SortKey.S]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Particular attributes from dynamo output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Items[*].[SortKey.S,SomeField.N]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[] | [.SortKey.S,.SomeField.N]]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter by field value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Items[?SortKey.S&amp;gt;'1674000000'].SomeField.N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[] | select(.SortKey.S&amp;gt;"1674000000").SomeField.N]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter on string prefix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Items[?starts_with(SortKey.S, 'TEXT')].SomeField.N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[] | select(.SortKey.S | startswith("TEXT")).SomeField.N]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want an example not using the data above, here is one you can run on your CloudFormation stacks right now. Each command (one JMES and one JQ) will show the last updated time of only your 'Dev'-stage stacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash-5.1$ aws cloudformation describe-stacks --query "Stacks[?contains(Tags[], {Key: 'STAGE', Value: 'dev'})].[StackName,LastUpdatedTime]"
bash-5.1$ aws cloudformation describe-stacks | jq '[.Stacks[] | select(.Tags[] | contains({Key: "STAGE", Value: "dev"})) | [.StackName,.LastUpdatedTime]]'
[
    [
        "my-sls-stack-dev",
        "2023-02-03T07:56:49.848Z"
    ],
    [
        "my-www-stack-dev",
        "2022-12-20T11:07:21.155Z"
    ]
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting more complex
&lt;/h3&gt;

&lt;p&gt;Let's do some sorting. Yes, they can do that, and have many other functions built in!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;AWS CLI query expression&lt;/th&gt;
&lt;th&gt;JQ Expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sort output numerically by field values*&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sort_by(Items[*], &amp;amp;to_number(SomeField.N))[*][SortKey.S,SomeField.N]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[] | [.SortKey.S,.ServiceTime.N]] | sort_by(.[1] | tonumber)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sum fields (e.g. get total page access time )&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum(map(&amp;amp;to_number(ServiceTime.N), Items[*]))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[].ServiceTime.N | tonumber] | add&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perform counting e.g. sum of pages accessed by Mozilla&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(Items[?AgentString &amp;amp;&amp;amp; starts_with(AgentString.S, 'Mozilla')])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[.Items[] | select(.AgentString.S | startswith("Mozilla"))] | length&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* &lt;em&gt;Note the expressions convert the fields to numbers here so as to sort numerically rather than textually&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This post shows that it is possible to perform some complicated transformations on JSON output data. I think most people could see that the above commands, which you can almost call 1-liners, can replace a whole JavaScript or Python function and allow you to perform complicated ad-hoc and maybe even regular tasks, with much less development overhead.&lt;/p&gt;

&lt;p&gt;In fact, because JQ can read your filter expression from a file (which can also contain comments), complex filters can turn into 1-liners, with JQ as your script interpreter. This also means that you can version-control and track your JQ scripts.&lt;/p&gt;

&lt;p&gt;The specifications of these tools are quite similar and are available in library form in JavaScript, Python, Go and many other languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Resources
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-filter.html" rel="noopener noreferrer"&gt;Filtering output from the AWS CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post was adapted from a larger post on my blog. &lt;a href="http://www.developdeploydeliver.com/blog/articles/2023-02-aws-cli-advanced-filtering/" rel="noopener noreferrer"&gt;See the full post here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>wordpress</category>
      <category>php</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
