Paired with AI: A senior developer’s journey building plugins

The author shares his experience using ChatGPT to learn Go, explore the Kolide API, and build a complex Steampipe plugin.

Translated from Pairing With AI: A Senior Developer's Journey Building a Plugin , author Jon Udell.

While improving developer documentation is always helpful, many people (myself included) prefer to learn by doing. This is the seventh and most important of my seven guiding principles : Because you acquire knowledge in task-oriented, teachable moments, learning is not proactive but immediate and tangible.

When experienced developers work with LLM, its machine intelligence supports and enhances your intelligence.

The benefits are obvious to me. Writing an ODBC plug-in for Steampipe in the LLM era was much easier than my experience writing plug-ins without such help. But that's admittedly a subjective assessment, so I was looking for an opportunity to compare notes with another plugin developer when James Ramirez showed up in our community Slack and announced a new plugin for the Kolide API . I invited him to tell me about his experience building it, and he graciously led me into a long conversation with ChatGPT, in which he became familiar with three technical areas that were new to him: the Kolide API, Go language and Steampipe plug-in architecture.

As an added challenge: while plugin developers usually find the right Go SDK for the API their plugin targets, that's not the case here. Therefore, it is necessary to create a Go wrapper for the Kolide API and then integrate it into the plugin.

Testing ChatGPT’s Go capabilities

James starts with some warm-up exercises. First, to test ChatGPT's Go capabilities, he provided a pair of Go functions that he wrote to call the relevant API_/devices/ and /devices/ID_, and required idiomatic refactoring of the shared logic between them.

Next, he explored the function's optional arguments using simple variadic arguments rather than the more complex function option pattern , and settled on a simple approach - using a slice of a _Search_ structure to encapsulate the fields of Kolide's query parameters / Operator/value style - that's enough. He asked for a function to serialize a slice of the Search structure into a REST URL, then optimized the version proposed by ChatGPT to create the final serializeSearches , which added support for mapping friendly names to parameters and using string builders.

AI handles nitpicking and often provides submissionable suggestions.

Some of these optimizations, including the use of string builders, were suggested by CodeRabbit , an AI-powered bot that provides helpful code reviews. This is feedback that helps you and your team focus on the big picture, he says, because it handles nitpicking and often (but not always) provides submittable suggestions. It also takes a broader perspective to summarize pull requests and evaluate whether a closed PR addressed the goals stated in its associated issue.

mapping operator

He continues to explore ways to map Steampipe operators (such as _QualOperatorEqual_) to Kolide operators (such as _Equals_). Likewise, the approach proposed by ChatGPT has become a one-off solution, moving towards a clean and simple solution. But as James confirmed in our interview, since you'll be iterating on one-off versions anyway, it's helpful to be able to generate sensible iterations rather than coding them more tediously by hand. Along the way, he's learning basic Go idioms.

James:

Are there do-while loops in Go?

ChatGPT

No, but...

James:

Is there a ternary operator in Go?

ChatGPT

No, but...

James:

How do I append to _map[string]string_?

ChatGPT

like this……

Using Reflection Enhanced Visitor Pattern

After understanding the basics and developing a Go client for the Kolide API, James was ready to tackle the real work of plugin development: defining the tables that map the Go types returned from the API wrapper to the Steampipe schema that controls SQL queries against those tables.

Like all plugin developers, he starts with a table that lists a collection of resources, then enhances it with filtering and pagination. After adding the second table, it's time to think about how to abstract out common patterns and behaviors. The end result is an elegant implementation of the Visitor pattern. Below are the Steampipe_List_ functions corresponding to the tables kolide_device and kolide_issue .

func listDevices(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
	var visitor ListPredicate = func(client *kolide.Client, cursor string, limit int32, searches ...kolide.Search) (interface{}, error) {
		return client.GetDevices(cursor, limit, searches...)
	}

	return listAnything(ctx, d, h, "kolide_device.listDevices", visitor, "Devices")
}


func listAdminUsers(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
	var visitor ListPredicate = func(client *kolide.Client, cursor string, limit int32, searches ...kolide.Search) (interface{}, error) {
		return client.GetAdminUsers(cursor, limit, searches...)
	}

	return listAnything(ctx, d, h, "kolide_admin_user.listAdminUsers", visitor, "AdminUsers")
}

The following are list functions common to all plugin tables.

func listAnything(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData, callee string, visitor ListPredicate, target string) (interface{}, error) {
	// Create a slice to hold search queries
	searches, err := query(ctx, d)
	if err != nil {
		plugin.Logger(ctx).Error(callee, "qualifier_operator_error", err)
		return nil, err
	}

	// Establish connection to Kolide client
	client, err := connect(ctx, d)
	if err != nil {
		plugin.Logger(ctx).Error(callee, "connection_error", err)
		return nil, err
	}

	// Iterate through pagination cursors, with smallest number of pages
	var maxLimit int32 = kolide.MaxPaging
	if d.QueryContext.Limit != nil {
		limit := int32(*d.QueryContext.Limit)
		if limit < maxLimit {
			maxLimit = limit
		}
	}

	cursor := ""

	for {
		// Respect rate limiting
		d.WaitForListRateLimit(ctx)

		res, err := visitor(client, cursor, maxLimit, searches...)
		if err != nil {
			plugin.Logger(ctx).Error(callee, err)
			return nil, err
		}

		// Stream retrieved results
		collection := reflect.ValueOf(res).Elem().FieldByName(target)
		if collection.IsValid() {
			for i := 0; i < collection.Len(); i++ {
				d.StreamListItem(ctx, collection.Index(i).Interface())

				// Context can be cancelled due to manual cancellation or the limit has been hit
				if d.RowsRemaining(ctx) == 0 {
					return nil, nil
				}
			}
		}

		next := reflect.ValueOf(res).Elem().FieldByName("Pagination").FieldByName("NextCursor")
		if next.IsValid() {
			cursor = next.Interface().(string)
		}

		if cursor == "" {
			break
		}
	}

	return nil, nil
}

With this setup, adding new tables to the plugin is almost entirely declarative: you only need to define the schema and KeyColumns, and the correlation operations that bridge between the where (or join) clause in the SQL query and the API-level filter symbol. Then write a tiny List function that defines an accessor and passes that function into a common listAnything function that encapsulates query parameter marshalling, connects to the API client, calls the API, and unpacks the response into a collection As well as the ability to iterate over a collection to transfer data items to Steampipe's external data wrapper.

James uses ChatGPT to enable the idiomatic implementation of visitor pattern in Go. This involves learning how to define a type for a visitor function and then declaring a function to satisfy the type. Each table visitor encapsulates a call to the API client and returns an interface. All of this is fairly generic, but the visitor's response is specific to the Go type of the wrapped API response, which means a different List function needs to be written for each table. How to avoid this situation? James asked: "The field reference on the res variable needs to be of a mutable type, specified at execution time. Can you suggest a way to do this?"

ChatGPT's suggestion (which he followed) was to use reflection so that calls to listAnything (like listAnything(ctx, d, h, "kolide_device.listDevices", visitor, "Devices")) can be passed a name ("Devices"), making listAnything Ability to access fields of the response structure in a type-independent manner, for example, here the Devices field.

    type DeviceListResponse struct {
      Devices    []Device   `json:"data"`
      Pagination Pagination `json:"pagination"`
    }

Because of this, listAnything finally lives up to its name and becomes a general Steampipe List function. This solution uses very little reflection and retains Go's strong type checking in both the API layer and the Steampipe layer.

What does LLM assistance really mean?

It certainly _does_ not mean that LLM wrote a plugin that embodies a complex design pattern based on the following prompt: "I need a Steampipe plugin for the Kolide API, please create it." To me, and to James, it The meaning is more interesting: "Let's discuss the process of writing a plug-in for the Kolide API." It's like talking to a rubber duck in order to think out loud about requirements and strategies. But LLM is a talking rubber duck . Sometimes the responses apply directly, sometimes they don't, but either way, they usually help you gain clarity.

As an experienced software engineer, James could have figured it out - but it would have taken much longer.

“Conversations required me to be very specific about the questions I was asking,” says James. Although he was starting from scratch with Go, he brought a wealth of experience that allowed him to quickly target and figure out which were the right questions to ask. As an experienced software engineer, James could have figured all of this out on his own. But it will take longer, and he will spend a lot of time reading articles and documents beforehand rather than learning by doing. And there may not be time! As I've heard from many others now, the acceleration an LLM provides often makes the difference between having an idea and being able to execute it.

James also mentioned an open source angle that I hadn't considered. Before LLM, he would not have done this in a completely public way. "I'm keeping it a secret until I'm more confident," he said, "but it's been there from the beginning and I'm glad it's there." That made early contact with the Turbot team possible.

This is not an automation story, but an augmentation story. When an experienced developer like James Ramirez works with LLM, its machine intelligence supports and amplifies his intelligence. The two work together - not just writing code, but more importantly, thinking about architecture and design.

This article was first published on Yunyunzhongsheng ( https://yylives.cc/ ), everyone is welcome to visit.

RustDesk suspends domestic services due to rampant fraud Apple releases M4 chip Taobao (taobao.com) restarts web version optimization work High school students create their own open source programming language as a coming-of-age gift - Netizens' critical comments: Relying on the defense Yunfeng resigned from Alibaba, and plans to produce in the future The destination for independent game programmers on the Windows platform . Visual Studio Code 1.89 releases Java 17. It is the most commonly used Java LTS version. Windows 10 has a market share of 70%, and Windows 11 continues to decline. Open Source Daily | Google supports Hongmeng to take over; open source Rabbit R1; Docker supports Android phones; Microsoft’s anxiety and ambitions; Haier Electric has shut down the open platform
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/6919515/blog/11105744