From AppleScriptObjectiveC to Swift pt. 2

Today kids, we get some REST…

Okay, the joke was right there, and no, there is no joke “too bad” for me to take. So in the previous post, we looked at initial defaults setup, both in ASOC and Swift. We didn’t do anything with them, but we have the initial setup done. There’s a few things we have to deal with outside of that before we actually get into defaults or UI setup.

Also, for everyone who says “just rewrite it in <different language I like better>, note that this series will be rather long before we get to what the ASOC version has today. There’s going to be a lot of work done to get to where we are right now. No new features. Just a lot of work to get to where we are today.

Nagios Manager interacts with Nagios servers via a REST API. So I send URLs with data and get data back. Sometimes I’m reading existing information, (GET), creating or adding information (POST) or removing information (DELETE). But everything revolves around that.

Within Swift, there’s two major components I’m using, at least initially: URLSession and JSONSerialization. The former handles the communication between the app and server(s), the latter handles parsing it out into a useful format, usually a dictionary, or NSDictionary in ObjC-land. While I do use JSONSerialization in ASOC, for the URLSession components, I “cheat” as it were, and use a handy trick from AppleScript: the “do shell script” command, which lets me just execute curl commands from the shell.

I could use URLSession within ASOC, but I never did, “do shell script” is simpler, although probably not easier, and it lets AppleScript handle some things for me automagically. So as before, I’ll split this post up into the ASOC code and the Swift code, with explanations of each. This post will be focused on the GET part of the communications, we’ll take on POST and DELETE later on.

Also, while one downside of Swift is that it does’t have a tool like Script Debugger, it does have playgrounds, which gives me some of the tools Script Debugger has. (There’s no proper debugging in playgrounds, and that is an actual shame, it would make them even more cool than they are.) Playgrounds are awesome for testing out code before putting it in the app.

Nagios REST URLs

Before we get into the code, an overview of how URLs are used within the Nagios REST API. Regardless of what a given URL is doing, they’re all broken down about the same way. There’s a “root” URL, a path that indicates the main section of what the URL is for, an API Key, and then the specific command properties. GET/POST/DELETE are used to indicate the primary action. In this example, I’m using a ‘system info’ URL, as it returns a small amount of data that’s easily used to ensure I’m getting the info I need in the correct form. By and large, this is a very large string that I end up coercing/converting into a Dictionary (Swift) or a record/NSDictionary (ASOC) so I can make better use of key-value pairs.

Here’s a sample of the URL, (sanitized, obvs):

http://<ipaddress or DNS name of server>/nagiosxi/api/v1/system/info?apikey=<very long, complex API key>&pretty=1″

That’s pretty simple, but it has the basics. In terms of application logic, there’s a handful of parts. The first is the IP address or DNS name of the server. I trust I don’t have to explain that one. The second is the /nagios/api/v1/ part. that will go after the IP address or DNS name of the server for every command, and we’ll use that as a constant in Swift, once we get going. The system/info? bit identifies what part of the API the URL is talking to. In this case, basic system info. For user functions, we’d use “system/user”, and for host functions, “objects/host”. The API key is the authentication mechanism for a given server, and why I may add one new feature into this, if it’s not that hard, namely encrypting that data. Maybe. (Security in monitoring software is a mess on a good day. But if I can manage to do the right thing, I try to. I’m not a very good programmer.)

Everything after the API key is the actual command parameters. In this case, there are none, one reason it makes for a useful test case. Finally, the “pretty=1” bit makes the return look like this:

{
“product”: “nagiosxi”,
“version”: “5.6.5”,
“version_major”: “5”,
“version_minor”: “6.5”,
“build_id”: “1563463555”
“release”: 5605
}

Instead of a single undifferentiated string. Much easier to read and use, especially for setting up key-value pairs.

ASOC communications

Here’s the basic code for sending a GET request and putting the results into a record (dictionary) in ASOC:

property theSMTableServerURL : "" --bound to server url column in table
property theSMTableServerAPIKey : "" --bound to server API Key column in table
====================================================================
set theSMSURL to theSelectedServer's theSMTableServerURL as text
set theSMSURL to current application's NSString's stringWithString:theSMSURL
set theSMInfoURL to theSMSURL's stringByAppendingString: "system/info?apikey="
set theSMServerInfoCommand to "/usr/bin/curl -XGET \"" & theSMInfoURL & theSMSelectedAPIKey & "&pretty=1\"" 
set theSMServerInfoJSONDict to my getJSONData:(theSMServerInfoCommand)

====================================================================
on getJSONData:theCurlCommand	
	set theReturnedJSON to do shell script theCurlCommand
	set theReturnedJSON to current application's NSString's stringWithString:theReturnedJSON
	set theReturnedJSONData to theReturnedJSON's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	set {theReturnedJSONDict, theError} to current application's NSJSONSerialization's JSONObjectWithData:theReturnedJSONData options:0 |error|:(reference)
	return theReturnedJSONDict
end getJSONData:

There’s really not a lot going here. The three sections offset by the “===” line are both parts of using curl in ASOC and in the app.

The first part is setting up the two properties we use. The server info, including URL and API key are in the server manager table in the UI. (Really, it’s in an array controller, but there’s no real code to demo that bit. At least not yet.)

theSMTableServerURL property is the IP address or DNS name of the server with “/nagiosxi/api/v1/” appended to it. All Nagios REST commands use that, so there’s no point in not storing it that way. Saves time.

theSMTableServerAPIKey property is the API key used for that server, also in the server manager table.

set theSMSURL to theSelectedServer's theSMTableServerURL as text

Is me doing two things. First, I’m avoiding manipulating the property directly. Secondly, I’m making sure I know theSMSURL is text. (AppleScript’s version, not Cocoa’s.) I found that ASOC has an annoying tendency to sometimes make this particular value text and sometimes it’s an NSSTring. The behaviors of both are rather different within ASOC, so I remove the uncertainty.

set theSMSURL to current application's NSString's stringWithString:theSMSURL

Is me converting AppleScript text to a proper NSString. We’ll need that, and again, this way I know what SMSURL is in terms of type.

set theSMInfoURL to theSMSURL's stringByAppendingString: "system/info?apikey="

Since NSString isn’t mutable, and NSMutableString gives me the irrits and this app is not time-sensitive in terms of execution, meh, create another NSString, but with the bits that make it a “I want basic info on the Nagios Server” command, along with the APIKey lead-in.

set theSMServerInfoCommand to "/usr/bin/curl -XGET \"" & theSMInfoURL & theSMSelectedAPIKey & "&pretty=1\""

Here’s where we build the full curl command. Since the command itself as used requires quotation marks in the command, we add those in by escaping them. the “&” is AppleScript’s string concatenation symbol. So when this is all done, theSMSServerInfoCommand is fully set up to send a request for basic server info to the Nagios server.

set theSMServerInfoJSONDict to my getJSONData:(theSMServerInfoCommand)

Here’s where we call the getJSONData function, or in AppleScript-ese, the handler. We’re passing it theSMServerInfoCommand string, and getting back theSMServerInfoJSONDict, which will be an NSDictionary, or AppleScript Record.

Now for the function. There’s not a lot of code, but there is some stuff happening. ASOC, as a rule, is slaved to Objective-C, so the calling conventions vaguely resemble ObjC, but without all the brackets and @-signs. I think it’s a bit more readable.

on getJSONData:theCurlCommand

The initial function definition. Note that we don’t have to specify the type of data theCurlCommand is. It’s just there, we either use it correctly or we do not. Handy in some ways, less-handy in others. Definitely lets you be lazy, so I like it. Life is too short for a high-level language not to do work for you.

set theReturnedJSON to do shell script theCurlCommand

This is the “do shell script” command, which lets us run curl in the shell environment. It uses sh, not bash or zsh. That can trip you up. It also doesn’t use your .profile or .zshrc files, so you really want to fully path everything. Really. Really.

The result of the command, assuming no error is set into theReturnedJSON. It may not actually be JSON, strictly speaking, but I have my theOwn theNeuroses with variable naming, and I’m the only one having to read this, so meh. What you get back looks like this:

{
“product”: “nagiosxi”,
“version”: “5.6.5”,
“version_major”: “5”,
“version_minor”: “6.5”,
“build_id”: “1563463555”
“release”: 5605
}

(just as a reminder)

set theReturnedJSON to current application's NSString's stringWithString:theReturnedJSON

Since do shell script is an AppleScript command, it returns AppleScript text. We need it to be an NSString, so we convert it to one. Since AppleScript is somewhat unconcerned about many types of coercion, we don’t have to use a different variable. theReturnedJSON used to be AppleScript text, now it is an NSString.

set theReturnedJSONData to theReturnedJSON's dataUsingEncoding:(current application's NSUTF8StringEncoding)

since NSJSONSerialization wants NSData and not NSString, we have to convert it to NSData here via NSString’s dataUsingEncoding method and NSUTF8StringEncoding as the specific encoding process. We do use a new variable here, so I know this is the NSData version of theReturnedJSON.

set {theReturnedJSONDict, theError} to current application's NSJSONSerialization's JSONObjectWithData:theReturnedJSONData options:0 |error|:(reference)

This returns an NSData record of arrays and shoves it in theReturnedJSONDict. Technically it’s an NSJSON object, but really, it functions more like an NSDictionary. Each line is a key-value pair, and this also allows it to work like an AppleScript Record, which is really handy for ASOC.

return theReturnedJSONDict

return theReturnedJSONDict to the calling function.

end getJSONData:

End the function/handler. the “:” functions as a “()” would in other languages. This is inconsistent mind you, sometimes, you’ll see “foo:()” or even “foo()“. The ASOC bridge is sometimes a tad odd.

Swift Communication

So some caveats here. The Swift code is very much playground code. It will look different in the final version, and once I get to where I feel like setting up the github repo for it, you’ll be able to see that. Again: I am not a full-time programmer, nor am I a Swift genius. I am very much a pickup truck programmer. It ain’t fancy, probably slower than it could be, but I can read it and understand it. It’s similar to how I play D&D: I understand the tricks I use as a monk very well. I don’t use a lot, but the ones I use, I really understand. Basically, I’m not clever. At all.

Also, many, many thanks to https://learnappmaking.com/urlsession-swift-networking-how-to/ for that page. It made it so much easier to figure out WTF was going on.

import Cocoa

//many thanks to https://learnappmaking.com/urlsession-swift-networking-how-to/ for this info.

//build the components to get data from nagios
//you can't use URLComponents with this, the encoding causes problems.

//we may be using url = url?.appendingPathComponent("users") kind of thing in the future

//this returns a small number of things from the nagios server
//we'll probably want this to be a var when we build it, made up of different items.

let theURL = URL(string: "http://(ipaddress or DNS name of the server)/nagiosxi/api/v1/system/info?apikey=(really long API key)&pretty=1")!

//create a URL Session object
let theSession = URLSession.shared

//theTask creates a data task. The parts are as follows:
//data - the actual JSON data we get from the server
//response - various response codes, mime types, etc
//error - any errors thrown
let theTask = theSession.dataTask(with: theURL) {data, response, error in
	
	//if we get an error or no data, bad. Probably won't use this in proudction
	if error != nil || data == nil {
		print("Client error!")
		return
	}
	//check for valid response code. Again probably won't use this
	guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
		print("Server error!")
		return
	}
	
	//check for the right mime type. Since we know how nagios returns things, not really needed
	guard let mime = response.mimeType, mime == "application/json" else {
		print("Wrong MIME type!")
		return
	}
	
	//this parses the returned data and turns it into  a proper JSON object
	do {
		//store contents of theURL as a Data variable
		let theData = try Data(contentsOf: theURL)
		
		//get the results as a dictionary, not a string
		let theJSON = try JSONSerialization.jsonObject(with: theData, options: []) as! [String:Any]
		
		
		//note that release version comes as an int from the rest api, not a string:
		/*
		{
			"product": "nagiosxi",
			"version": "5.6.5",
			"version_major": "5",
			"version_minor": "6.5",
			"build_id": "1563463555",
			"release": 5605
		}
		*/
		
		//set up the variables to extract the data from the dictionary
		var theProduct: Any
		var theBuildID: String
		var theVersion: String
		var theRelease: Any
		var theMajorVersion: String
		var theMinorVersion: String
		
		//pull our vars from the dictionary
		//since these could be nil, we force unwrap at theJSON and use as! to force downcast AnyObject to string
		//or Int as needed
		
		//it is probably a good idea to make sure your variables are what you think they are
		//this can avoid problems later. But this does show you have options if you're not sure
		//what the return will actually be
		theProduct = theJSON["product"]! //as! String
		theBuildID = theJSON["build_id"] as! String
		theVersion = theJSON["version"] as! String
		//even though we created the var as an Any, we force it to be an Int here. 
		theRelease = theJSON["release"] as! Int
		theMajorVersion = theJSON["version_major"] as! String
		theMinorVersion = theJSON["version_minor"] as! String
		
		print(theProduct)
		print(theBuildID)
		print(theVersion)
		print(theRelease)
		print(theMajorVersion)
		print(theMinorVersion)
		
		//print(theJSON)

}

theTask.resume()

There’s a lot going on here. Well, not really, but it took me a while to get it. (That was reason why I didn’t use NSURLSession in the ASOC version. It was weird to learn. Well, also, I didn’t have to.)

Some of it, like the setup for the print statements is test code, so I know what’s going on with it. That most likely won’t be that way in the final version, but it is useful here.

let theURL = URL(string: "http://(ipaddress or DNS name of the server)/nagiosxi/api/v1/system/info?apikey=(really long API key)&pretty=1")!

Since this is a constant, we use “let”. This will eventually be built the way it is in the ASOC version. But for now, we just hardcode the whole thing.

let theSession = URLSession.shared

Create a simple URLSession object. Should I need to get more complicated, I will, but not until.

let theTask = theSession.dataTask(with: theURL) {data, response, error in

Create a data task that when run, gives us three things:

1) data, the actual returned JSON data
2) response, the HTTP response info wif error != nil || data == nil {ith things like return codes, etc.
3) error, I feel this is self-explanatory. At best, we’ll only care if it’s not nil.

Maybe not even then. Nagios Manager isn’t the kind of thing you just run casually. I can make more than a few assumptions.

if error != nil || data == nil {
print("Client error!")
return
}


If we get any kind of error or no data, print an error message and return.

(can I just say wordpress’s block editor handling of returns is annoying as hell. No you goddamned schmuck, just because there’s a return when I paste something in, that does NOT mean I want it in a separate block. Ye gods.)

guard let response = response as? HTTPURLResponse, (200…
299).contains(response.statusCode) else {
print("Server error!")
return
}


if we don’t get a “correct” response code, do error things

guard let mime = response.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}


If the MIME type is wrong, do error things.

do {
//store contents of theURL as a Data variable
let theData = try Data(contentsOf: theURL
let theJSON = try JSONSerialization.jsonObject(with: theData, options: []) as! [String:Any]


Here, I’m setting up “Data” as a Data object version of theURL. Since the “contents” of the URL is the returned data, which looks like a big string to swift, we want it to be a dictionary, with the keys being String and the values being Any, as we get multiple return types. This is kind of a pain, since we don’t get ASOC’s “I’ll handle that for you” features, but it’s not awful. (I may be wrong about what this code does. But it does work the way I expect it to work, so close enough. URLSession is weird to me.)

//set up the variables to extract the data from the dictionary
var theProduct: Any
var theBuildID: String
var theVersion: String
var theRelease: Any
var theMajorVersion: String
var theMinorVersion: String


So out of all of these, only theRelease isn’t.a string, it’s an int. This is me playing a bit with Swift, because while I’ve read books and done stuff, this is the first time I’m actually using it.

//it is probably a good idea to make sure your variables are what you think they are
//this can avoid problems later. But this does show you have options if you're not sure what the return will actually be

theProduct = theJSON["product"]! //as! String
theBuildID = theJSON["build_id"] as! String
theVersion = theJSON["version"] as! String
theRelease = theJSON["release"] as! Int
theMajorVersion = theJSON["version_major"] as! String
theMinorVersion = theJSON["version_minor"] as! String


If you can’t tell, I talk to myself a lot in comments. I also walk my way through problems that way. Sometimes I put recipes in my comments. I’m odd like that. This is just me extracting things from a dict and documenting it so when I do it later, I know how. Note me coercing things. Except for theProduct, which was a “I wonder what will happen if I leave it as Any. Turns out, not much. But I won’t do that in the app, seems like a bad idea.

print(theProduct)
print(theBuildID)
print(theVersion)
print(theRelease)
print(theMajorVersion)
print(theMinorVersion)


PRINT ALL THE THINGS

} catch {
//print any errors
print("JSON error: (error.localizedDescription)")
}


Do error things.

}
theTask.resume()


Actually run the task, and get the JSON that lets us do the things. Y’all, I get that it works and it looks the same in ObjC, but this is kind of weird. Just saying.

As I said, this is not how it will look in the app, but it helps me see how swift does things. I’ll probably wrap up the JSON code in its own function and maybe even wrap up the URLSession code in its own function. Or not. Depends on how much of a pain in the ass this is.

The next bit of this will probably delve into the initial UI creation. Unless it doesn’t. And if you read this, thanks!