order_resource.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package provider
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. "github.com/hashicorp-demoapp/hashicups-client-go"
  8. "github.com/hashicorp/terraform-plugin-framework/resource"
  9. "github.com/hashicorp/terraform-plugin-framework/resource/schema"
  10. "github.com/hashicorp/terraform-plugin-framework/types"
  11. )
  12. // Ensure the implementation satisfies the expected interfaces.
  13. var (
  14. _ resource.Resource = &orderResource{}
  15. _ resource.ResourceWithConfigure = &orderResource{}
  16. )
  17. // NewOrderResource is a helper function to simplify the provider implementation.
  18. func NewOrderResource() resource.Resource {
  19. return &orderResource{}
  20. }
  21. type orderResource struct {
  22. client *hashicups.Client
  23. }
  24. func (r *orderResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
  25. // Add a nil check when handling ProviderData because Terraform
  26. // sets that data after it calls the ConfigureProvider RPC.
  27. if req.ProviderData == nil {
  28. return
  29. }
  30. client, ok := req.ProviderData.(*hashicups.Client)
  31. if !ok {
  32. resp.Diagnostics.AddError("Unexpected Resource Configure Type",
  33. fmt.Sprintf("Expected *hashicups.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData))
  34. return
  35. }
  36. r.client = client
  37. }
  38. func (r *orderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
  39. resp.TypeName = req.ProviderTypeName + "_order"
  40. }
  41. func (r *orderResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
  42. resp.Schema = schema.Schema{
  43. Attributes: map[string]schema.Attribute{
  44. "id": schema.StringAttribute{
  45. Computed: true,
  46. },
  47. "last_updated": schema.StringAttribute{
  48. Computed: true,
  49. },
  50. "items": schema.ListNestedAttribute{
  51. Required: true,
  52. NestedObject: schema.NestedAttributeObject{
  53. Attributes: map[string]schema.Attribute{
  54. "quantity": schema.Int64Attribute{
  55. Required: true,
  56. },
  57. "coffee": schema.SingleNestedAttribute{
  58. Required: true,
  59. Attributes: map[string]schema.Attribute{
  60. "id": schema.Int64Attribute{
  61. Required: true,
  62. },
  63. "name": schema.StringAttribute{
  64. Computed: true,
  65. },
  66. "teaser": schema.StringAttribute{
  67. Computed: true,
  68. },
  69. "description": schema.StringAttribute{
  70. Computed: true,
  71. },
  72. "price": schema.Float64Attribute{
  73. Computed: true,
  74. },
  75. "image": schema.StringAttribute{
  76. Computed: true,
  77. },
  78. },
  79. },
  80. },
  81. },
  82. },
  83. },
  84. }
  85. }
  86. func (r *orderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
  87. var plan orderResourceModel
  88. // 1. Checks whether the API Client is configured.
  89. if r.client == nil {
  90. resp.Diagnostics.AddError(
  91. "cannot create order",
  92. "client is not configured in orderResource.Create",
  93. )
  94. return
  95. }
  96. // 2. Retrieves values from the plan.
  97. // The function will attempt to retrieve values from the plan and convert it to an orderResourceModel.
  98. // If not, the resource responds with an error.
  99. diags := req.Plan.Get(ctx, &plan)
  100. resp.Diagnostics.Append(diags...)
  101. if resp.Diagnostics.HasError() {
  102. return
  103. }
  104. // 3. Generates an API request body from the plan values.
  105. // The function loops through each plan item and maps it to a hashicups.OrderItem.
  106. // This is what the API client needs to create a new order.
  107. var items []hashicups.OrderItem
  108. for _, item := range plan.Items {
  109. items = append(items, hashicups.OrderItem{
  110. Coffee: hashicups.Coffee{
  111. ID: int(item.Coffee.ID.ValueInt64()),
  112. },
  113. Quantity: int(item.Quantity.ValueInt64()),
  114. })
  115. }
  116. // 4. Creates a new order.
  117. // The function invokes the API client's CreateOrder method.
  118. order, err := r.client.CreateOrder(items)
  119. if err != nil {
  120. resp.Diagnostics.AddError(
  121. "Error creating order",
  122. err.Error(),
  123. )
  124. return
  125. }
  126. // 5. Maps response body to resource schema attributes.
  127. // After the function creates an order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
  128. plan.ID = types.StringValue(strconv.Itoa(order.ID))
  129. for orderItemIndex, orderItem := range order.Items {
  130. plan.Items[orderItemIndex] = orderItemModel{
  131. Coffee: orderItemCoffeeModel{
  132. ID: types.Int64Value(int64(orderItem.Coffee.ID)),
  133. Name: types.StringValue(orderItem.Coffee.Name),
  134. Teaser: types.StringValue(orderItem.Coffee.Teaser),
  135. Description: types.StringValue(orderItem.Coffee.Description),
  136. Price: types.Float64Value(orderItem.Coffee.Price),
  137. Image: types.StringValue(orderItem.Coffee.Image),
  138. },
  139. Quantity: types.Int64Value(int64(orderItem.Quantity)),
  140. }
  141. }
  142. plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // Why not RFC3339 ?
  143. // 6. Sets Terraform's state with the new order's details.
  144. diags = resp.State.Set(ctx, plan)
  145. resp.Diagnostics.Append(diags...)
  146. // Useless code in tutorial, why ?
  147. if resp.Diagnostics.HasError() {
  148. return
  149. }
  150. }
  151. func (r *orderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
  152. var state orderResourceModel
  153. // 0. Checks whether the API Client is configured.
  154. if r.client == nil {
  155. resp.Diagnostics.AddError(
  156. "cannot read order",
  157. "client is not configured in orderResource.Read",
  158. )
  159. return
  160. }
  161. // 1. Gets the current state.
  162. // If it is unable to, the provider responds with an error.
  163. diags := req.State.Get(ctx, &state)
  164. resp.Diagnostics.Append(diags...)
  165. if resp.Diagnostics.HasError() {
  166. return
  167. }
  168. // 2. Retrieves the order ID from Terraform's state.
  169. id := state.ID.ValueString()
  170. // 3. Retrieves the order details from the client.
  171. // The function invokes the API client's GetOrder method with the order ID.
  172. order, err := r.client.GetOrder(id)
  173. if err != nil {
  174. resp.Diagnostics.AddError(
  175. "Error reading HashiCups order",
  176. "Could not read HashiCups order "+state.ID.ValueString()+": "+err.Error(),
  177. )
  178. return
  179. }
  180. // 4. Maps the response body to resource schema attributes.
  181. // == overwrite items with refreshed state
  182. // After the function retrieves the order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
  183. state.Items = []orderItemModel{}
  184. for _, item := range order.Items {
  185. itemState := orderItemModel{
  186. Coffee: orderItemCoffeeModel{
  187. ID: types.Int64Value(int64(item.Coffee.ID)),
  188. Name: types.StringValue(item.Coffee.Name),
  189. Teaser: types.StringValue(item.Coffee.Teaser),
  190. Description: types.StringValue(item.Coffee.Description),
  191. Price: types.Float64Value(item.Coffee.Price),
  192. Image: types.StringValue(item.Coffee.Image),
  193. },
  194. Quantity: types.Int64Value(int64(item.Quantity)),
  195. }
  196. state.Items = append(state.Items, itemState)
  197. }
  198. // 5. Set Terraform's state with the order's model.
  199. diags = resp.State.Set(ctx, state)
  200. resp.Diagnostics.Append(diags...)
  201. // Useless code in tutorial, why ?
  202. if resp.Diagnostics.HasError() {
  203. return
  204. }
  205. }
  206. func (r *orderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
  207. return
  208. }
  209. func (r *orderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
  210. return
  211. }
  212. // The *Model types describe the types known by the API.
  213. // They are basically DTOs for the TF provider to talk to the API, while
  214. // its native type is the Plan with its Schema.
  215. type orderResourceModel struct {
  216. ID types.String `tfsdk:"id"`
  217. Items []orderItemModel `tfsdk:"items"`
  218. LastUpdated types.String `tfsdk:"last_updated"`
  219. }
  220. type orderItemModel struct {
  221. Quantity types.Int64 `tfsdk:"quantity"`
  222. Coffee orderItemCoffeeModel `tfsdk:"coffee"`
  223. }
  224. type orderItemCoffeeModel struct {
  225. ID types.Int64 `tfsdk:"id"`
  226. Name types.String `tfsdk:"name"`
  227. Teaser types.String `tfsdk:"teaser"`
  228. Description types.String `tfsdk:"description"`
  229. Price types.Float64 `tfsdk:"price"`
  230. Image types.String `tfsdk:"image"`
  231. }