I am using RestKit 0.20.0rc1 in an app that uses 2 entities:
A "note" (NoteClass).
A "set" (SetClass), which contains a collection of notes.
I have the following 2 response descriptors (among others):
// GET /sets/:setID/notes
// Get a set's notes. Response looks like this:
// {"notes": [ (array of NoteClass dictionaries) ],
// ...more stuff...
// }
RKResponseDescriptor *noteResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[NoteClass rkEntityMapping]
pathPattern:#"/sets/:setID/notes"
keyPath:#"notes"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:noteResponseDescriptor];
// GET /sets/:setID
// Get information about a set. Response looks like this:
// {"name": "My Set",
// "numNotes": 3,
// ...more stuff...
// }
RKResponseDescriptor *setResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[SetClass rkEntityMapping]
pathPattern:#"/sets/:setID"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:setResponseDescriptor];
When I request "/sets/:setID/notes", the noteResponseDescriptor matches (expected). However, the setResponseDescriptor also matches (unexpected). I believe this is because the response descriptor's path pattern matches the substring "/sets/:setID", and because key path is nil. As a result, when I make the request, the RKMappingResult I get back contains an array of NoteClass objects (expected) and a single empty SetClass object (unexpected).
How do I prevent the setResponseDescriptor from matching this notes endpoint? I can't add a key path to the setResponseDescriptor (yet), so my preference is for a solution that allows me to say something like "match /sets/:setID$", where "$" designates the end of the URL.
Turns out that there is now way to prevent multiple matches for your example (see the discussion here).
While a solution is on the way you have a few options for fixing this:
Change your API urls so it doesn't lead to ambiguities
Check the type of the mapped result objects and discard any unexpected objects
Modify [RKResponseDescriptor matchesPath:] as suggested in the discussion thread.
Each solution is flawed in it's own way though.
Related
I have set up a number of intercepts in the body of my tests. Here they are, pasted from the Cypress log, with the alias added
cy:intercept ➟ // alias: getRecipesSku(971520)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields**"
Mocked Response: [{ ... }]
cy:intercept ➟ // alias: getRecipesSku(971520,971310)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields**"
Mocked Response: [{ ... }]
Our application's tests also mocks a number of routes by default, (coming from an apiClient.initialize) including this one below. FWIW, this is defined earlier than those above:
cy:intercept ➟ // alias: getJustcookRecipes
Method: GET
Matcher: "https://wpsite.com/wp-json/**"
Mocked Response: [{ ... }]
My test code sets up those first two intercepts, and ultimately calls the routes. Here is the code, heavily abridged:
it('refreshes the recipes when switching protein tabs', () => {
apiClient.initialize()
/* do lots of other stuff; load up the page, perform other tests, etc */
// call a function that sets up the intercepts. you can see from the cypress output
// that the intercepts are created correctly, so I don't feel I need to include the code here.
interceptJustCook({ skus: [beefCuts[0].id] }, [beefCut1Recipe])
interceptJustCook({ skus: [beefCuts[0].id, beefCuts[1].id] }, twoBeefRecipes)
// [#1] select 1 item;
// calls route associated with intercept "getRecipesSku(971520)"
page.click.checkboxWithSku(beefCuts[0].id)
/* assert against that call */
// [#2] select 2nd item (now 2 items are selected);
// calls route associated with intercept "getRecipesSku(971520, 971310)"
page.click.checkboxWithSku(beefCuts[1].id)
In the Cypress logs, the first call (marked by comment #1) is intercepted correctly:
cy:fetch ➟ (getRecipesSku(971520)) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
However, the second call (marked by comment #2) is intercepted by the wrong route mocker:
cy:fetch ➟ (getJustCookRecipes) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
You can see for yourself that the URL called at #2 does indeed match the getRecipesSku(971520, 971310) intercept, but it is caught by the getJustcookRecipes intercept. Now, I suppose the URL for that latter intercept would catch my second custom intercept. But it would also, in the same way, catch my first custom intercept, but that first one works.
(update:) I tried commenting out the place in the code where the getJustcookRecipes intercept is created so that it doesn't exist. Now, the call that should hit getRecipesSku(971520,971310) isn't being mocked at all! I checked and the mocked and called urls are a match.
Why is this going wrong and how do I fix it?
Something in the glob pattern for the 2nd intercept #getRecipesSku(971520,971310) is refusing to match.
It's probably not worth while analyzing what exactly (you may not be able to fix it in glob), but switching to a regex will match.
See regex101.com online test
cy.intercept(/wpsite\.com\/wp-json\/wp\/v2\/posts\?tags\[]=6287&tags\[]=6289&_fields/, {})
.as('getRecipesSku(971520,971310)')
The request query string may be badly formed
Looking at the docs for URLSearchParams, the implication is that the query string should be key/value pairs.
But the 2nd request has two identical keys using tags[] as the key.
It looks as if the correct format would be /wp/v2/posts?tags=[6287,6289] since the square brackets don't have a lot of meaning otherwise.
It may be that the server is handling the format tags[]=6287&tags[]=6289, but Cypress is probably not. If you run the following intercept you see the query object has only one tags[] key and it's the last one in the URL.
cy.intercept({
method: 'GET',
pathname: '/wp-json/wp/v2/posts',
},
(req) => {
console.log('url', req.url)
console.log('query', req.query) // Proxy {tags[]: '6289', ...
}
)
#Paolo was definitely on the right track. The WP API we're consuming uses tags[]=1,tags=[2] instead of tags[1,2] so that's correct, but some research into globs showed me that brackets are special. Why the first bracket isn't messing it up but the second one is, I'm not sure and I kind of don't care.
I thought about switching to regex, but instead I was able to escape the offender in the glob without switching to regex and needing to escape aaaallll the slashes in the url:
const interceptRecipes = (
{ category, skus }: RecipeFilterArgs,
recipes: Recipe[]
) => {
let queryString = ''
if (category) queryString = `categories[]=${CATEGORY_MAP[category]}`
if (skus) {
const tagIdMap = SkuToTagIdMap as Record<number, { tagId: number }>
// brackets break the glob pattern, so we need to escape them
queryString = skus.map((sku) => `tags\\[]=${tagIdMap[sku].tagId}`).join('&')
}
const url = `${baseUrl}?${queryString}${otherParams}`
const alias = category ? `get${category}Recipes` : `getRecipesSku(${skus})`
cy.intercept('GET', url, recipes).as(alias)
}
I have an API get requests in Postman that uses a data file of voucher codes to look up other information about the code, such as the name of the product the code is for. When using collection runner the voucher codes are passed incorrectly and the data is returned about the product.
For some reason, I'm unable to capture the data from the response body and link this into the next request.
1st get request has this in the body section:
{
"dealId": 6490121,
"voucherCode": "J87CM9-5PV33M",
"productId": 520846,
"productTitle": "A Book",
"orderNumber": 23586548,
"paymentMethod": "Braintree",
"deliveryNotificationAvailable": true
}
I have this in the tests section to capture the values:
var jsonData = pm.response.json()
pm.environment.set("dealId", jsonData.dealId);
pm.globals.set("productId", jsonData.productId);
when posting the next request in the body:
{
"dealId":{{dealId}},
"dealVoucherProductId": {{productId}},
"voucherCode":"{{VoucherCode}}",
}
and pre-request scripts:
pm.environment.set("productId", "productId");
pm.globals.set("dealId", "dealId");
As you can see I've tried to use global and environmental variables both are not populating the next request body.
What am I missing?
This wouldn't set anything in those variables apart from the strings that you've added.
pm.environment.set("dealId", "dealId");
pm.globals.set("productId", "productId");
In order to capture the response data and set it in the variable you will need to add something like this to the first requests Tests tab:
var jsonData = pm.response.json()
pm.environment.set("dealId", jsonData.dealId);
pm.globals.set("productId", jsonData.productId);
Depending on the response schema of the first request - This should set those values as the variables.
You can just use the {{dealId}} and {{productId}} where ever you need them after that.
If you're using a environment variable, ensure that you have created an file for those values to be set.
I can get single "article" or an array of "articles". Mapping is the same. But keyPath will be "article" or "articles". Do I need 2 different descriptors? Or can I combine it into one somehow? (as an example - Ruby on Rails can recognize single-multiple automatically).
Currently I'm doing it separately, Single:
RKResponseDescriptor *articleDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:articleMapping method:RKRequestMethodAny pathPattern:nil keyPath:#"article" statusCodes:statusCodes];
[_manager addResponseDescriptor:articleDescriptor];
Multiple:
RKResponseDescriptor *articlesArrayDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:articleMapping method:RKRequestMethodAny pathPattern:nil keyPath:#"articles" statusCodes:statusCodes];
[_manager addResponseDescriptor:articlesArrayDescriptor];
You need multiple descriptors. Usually you would want to set the pathPattern parameter to something specific on your response descriptors anyway to aid performance and prevent the creating of unwanted objects as always setting the pathPattern to nil will mean that RestKit can't filter out invalid descriptors for the received data and will need to process them. So, any overlap of key paths in your responses will start to generate (usually empty) object creations that you don't want.
How do I create an if statement saying something like this?
Basically, how do you use the URI class to determine if there is a value in any segment?
$segment = value_of_any_segment;
if($segment == 1{
do stuff
}
I know this is pretty elementary, but I don't totally understand the URI class...
Your question is a little unclear to me, but I'll try to help. Are you wondering how to determine if a particular segment exists or if it contains a specific value?
As you are probably aware, you can use the URI class to access the specific URI segments. Using yoursite.com/blog/article/123 as an example, blog is the 1st segment, article is the 2nd segment, and 123 is the 3rd segment. You access each using $this->uri->segment(n)
You then can construct if statements like this:
// if segment 2 exists ("articles" in the above example), do stuff
if ($this->uri->segment(2)) {
// do stuff
}
// if segment 3 ("123" in the above example) is equal to some value, do stuff
if ($this->uri->segment(3) == $myValue) {
// do stuff
}
Hope that helps! Let me know if not and I can elaborate or provide additional information.
Edit:
If you need to determine if a particular string appears in any segment of the URI, you can do something like this:
// get the entire URI (using our example above, this is "/blog/article/123")
$myURI = $this->uri->uri_string()
// the string we want to check the URI for
$myString = "article";
// use strpos() to search the entire URI for $myString
// also, notice we're using the "!==" operator here; see note below
if (strpos($myURI, $myString) !== FALSE) {
// "article" exists in the URI
} else {
// "article" does not exist in the URI
}
A note regarding strpos() (from the PHP documentation):
This function may return Boolean
FALSE, but may also return a
non-Boolean value which evaluates to
FALSE, such as 0 or "". Please read
the section on Booleans for more
information. Use the === operator for
testing the return value of this
function.
I hope my edit helps. Let me know if I can elaborate.
I'm having success when I use this code to get a string from an array of file names called "fileList":
cell.timeBeganLabel.text = [[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension];
so I expected the same code to generate the same string as a key for me in this:
NSDictionary *stats = [thisRecordingsStats objectForKey:[[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension]];
cell.durationLabel.text = [stats objectForKey:#"duration"];
or this:
NSDictionary *stats = [thisRecordingsStats objectForKey:#"%#",[[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension]];
Both build without error, and the log shows my data is there: but I'm getting a blank UILabel.
Have I not written the dynamic key generator correctly?
I'm having success when I use this code to get a string from an array of file names called "fileList":
cell.timeBeganLabel.text = [[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension];
So, the result of that message expression is your key, right?
That is to say, the keys in your dictionary are filenames without extensions?
so I expected the same code to generate the same string as a key for me in this:
NSDictionary *stats = [thisRecordingsStats objectForKey:[[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension]];
cell.durationLabel.text = [stats objectForKey:#"duration"];
You compute the filename without extension as before.
You look up the object for this string in the thisRecordingsStats dictionary, thus obtaining another dictionary, with which you initialize the stats variable.
You look up the object for the “duration” key in the stats dictionary, and set the durationLabel's text to this object.
or this:
NSDictionary *stats = [thisRecordingsStats objectForKey:#"%#",[[[self.fileList objectAtIndex:[indexPath row]] lastPathComponent] stringByDeletingPathExtension]];
Adding the #"%#", part doesn't make sense, since objectForKey: doesn't take a format string. Compare the documentation for NSString's stringWithFormat: method to the documentation for NSDictionary's objectForKey: method.
The code “works” because what you have passed as the argument to objectForKey: is a comma expression. C's comma operator evaluates both sides and evaluates to the right-hand side. However, in this case as in most others, it adds nothing. For reasons like this, the comma operator is rarely used and even more rarely used on purpose.
Cut the #"%#", part out.
Back to the problem:
Both build without error, and the log shows my data is there: but I'm getting a blank UILabel.
Well, you say the key you're generating from the string in your fileList array shows up in the UILabel, so the problem is one of these:
thisRecordingStats is nil.
thisRecordingStats does not contain an object for the key you generated from the string in self.fileList.
thisRecordingStats does contain an object for the key you generated from the string in self.fileList, and it is a dictionary, but it does not contain a value for the key “duration”.
thisRecordingStats does contain an object for the key you generated from the string in self.fileList, and it is a dictionary, and it contains a value for the key “duration”, but that value is an empty (zero-length) string.
You should also check the Debugger Console for messages that suggest other problems. For example, a “does not respond to selector” message may be because thisRecordingStats contains an object for the key you generated from the string in self.fileList, but it is not a dictionary.
Finally, I suggest constructing one or more model object classes instead of nesting dictionaries like this. It tends to make the code much easier to read and to debug. In particular, the dictionaries that ostensibly have objects for the key “duration” should be model objects.