-
Notifications
You must be signed in to change notification settings - Fork 400
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement API, saga and reducer code to interact with TAAR Lite servi…
…ce via addons-server (#4904)
- Loading branch information
1 parent
6313cae
commit 1d66889
Showing
9 changed files
with
486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* @flow */ | ||
import invariant from 'invariant'; | ||
|
||
import { callApi } from 'core/api'; | ||
import type { ApiStateType } from 'core/reducers/api'; | ||
import type { PaginatedApiResponse } from 'core/types/api'; | ||
import type { ExternalAddonType } from 'core/types/addons'; | ||
|
||
|
||
export type GetRecommendationsParams = {| | ||
api: ApiStateType, | ||
guid: string, | ||
recommended: boolean, | ||
|}; | ||
|
||
export const getRecommendations = ( | ||
{ api, guid, recommended }: GetRecommendationsParams | ||
): Promise<PaginatedApiResponse<ExternalAddonType>> => { | ||
invariant(guid, 'A guid is required.'); | ||
invariant(typeof recommended === 'boolean', 'recommended is required'); | ||
|
||
return callApi({ | ||
auth: true, | ||
endpoint: 'addons/recommendations/', | ||
params: { guid, recommended }, | ||
state: api, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
/* @flow */ | ||
import invariant from 'invariant'; | ||
|
||
import { createInternalAddon } from 'core/reducers/addons'; | ||
import type { AddonType, ExternalAddonType } from 'core/types/addons'; | ||
|
||
export const ABORT_FETCH_RECOMMENDATIONS: 'ABORT_FETCH_RECOMMENDATIONS' | ||
= 'ABORT_FETCH_RECOMMENDATIONS'; | ||
export const FETCH_RECOMMENDATIONS: 'FETCH_RECOMMENDATIONS' | ||
= 'FETCH_RECOMMENDATIONS'; | ||
export const LOAD_RECOMMENDATIONS: 'LOAD_RECOMMENDATIONS' | ||
= 'LOAD_RECOMMENDATIONS'; | ||
|
||
export type FallbackReasonType = 'no_results' | 'timeout'; | ||
export type OutcomeType = 'curated' | 'recommended' | 'recommended_fallback'; | ||
|
||
export type Recommendations = {| | ||
addons: Array<AddonType> | null, | ||
fallbackReason: FallbackReasonType | null, | ||
loading: boolean, | ||
outcome: OutcomeType | null, | ||
|}; | ||
|
||
export type RecommendationsState = {| | ||
byGuid: { | ||
[guid: string]: Recommendations, | ||
}, | ||
|}; | ||
|
||
export const initialState: RecommendationsState = { | ||
byGuid: {}, | ||
}; | ||
|
||
export type AbortFetchRecommendationsParams = {| | ||
guid: string, | ||
|}; | ||
|
||
type AbortFetchRecommendationsAction = {| | ||
type: typeof ABORT_FETCH_RECOMMENDATIONS, | ||
payload: AbortFetchRecommendationsParams, | ||
|}; | ||
|
||
export const abortFetchRecommendations = ({ | ||
guid, | ||
}: AbortFetchRecommendationsParams): AbortFetchRecommendationsAction => { | ||
invariant(guid, 'guid is required'); | ||
return { | ||
type: ABORT_FETCH_RECOMMENDATIONS, | ||
payload: { guid }, | ||
}; | ||
}; | ||
|
||
type FetchRecommendationsParams = {| | ||
errorHandlerId: string, | ||
guid: string, | ||
recommended: boolean, | ||
|}; | ||
|
||
export type FetchRecommendationsAction = {| | ||
type: typeof FETCH_RECOMMENDATIONS, | ||
payload: FetchRecommendationsParams, | ||
|}; | ||
|
||
export const fetchRecommendations = ({ | ||
errorHandlerId, | ||
guid, | ||
recommended, | ||
}: FetchRecommendationsParams): FetchRecommendationsAction => { | ||
invariant(errorHandlerId, 'errorHandlerId is required'); | ||
invariant(guid, 'guid is required'); | ||
invariant(typeof recommended === 'boolean', 'recommended is required'); | ||
|
||
return { | ||
type: FETCH_RECOMMENDATIONS, | ||
payload: { errorHandlerId, guid, recommended }, | ||
}; | ||
}; | ||
|
||
export type LoadRecommendationsParams = {| | ||
addons: Array<ExternalAddonType>, | ||
fallbackReason: string, | ||
guid: string, | ||
outcome: string, | ||
|}; | ||
|
||
type LoadRecommendationsAction = {| | ||
type: typeof LOAD_RECOMMENDATIONS, | ||
payload: LoadRecommendationsParams, | ||
|}; | ||
|
||
export const loadRecommendations = ({ | ||
addons, | ||
fallbackReason, | ||
guid, | ||
outcome, | ||
}: LoadRecommendationsParams): LoadRecommendationsAction => { | ||
invariant(addons, 'addons is required'); | ||
invariant(guid, 'guid is required'); | ||
invariant(outcome, 'outcome is required'); | ||
|
||
return { | ||
type: LOAD_RECOMMENDATIONS, | ||
payload: { addons, guid, outcome, fallbackReason }, | ||
}; | ||
}; | ||
|
||
type GetRecommendationsByGuidParams = {| | ||
guid: string, | ||
state: RecommendationsState, | ||
|}; | ||
|
||
export const getRecommendationsByGuid = ( | ||
{ guid, state }: GetRecommendationsByGuidParams | ||
): Recommendations | null => { | ||
invariant(guid, 'guid is required'); | ||
invariant(state, 'state is required'); | ||
|
||
return state.byGuid[guid] || null; | ||
}; | ||
|
||
type Action = | ||
| AbortFetchRecommendationsAction | ||
| FetchRecommendationsAction | ||
| LoadRecommendationsAction; | ||
|
||
const reducer = ( | ||
state: RecommendationsState = initialState, | ||
action: Action | ||
): RecommendationsState => { | ||
switch (action.type) { | ||
case ABORT_FETCH_RECOMMENDATIONS: | ||
return { | ||
...state, | ||
byGuid: { | ||
...state.byGuid, | ||
[action.payload.guid]: { | ||
addons: null, | ||
fallbackReason: null, | ||
loading: false, | ||
outcome: null, | ||
}, | ||
}, | ||
}; | ||
|
||
case FETCH_RECOMMENDATIONS: | ||
return { | ||
...state, | ||
byGuid: { | ||
...state.byGuid, | ||
[action.payload.guid]: { | ||
addons: null, | ||
fallbackReason: null, | ||
loading: true, | ||
outcome: null, | ||
}, | ||
}, | ||
}; | ||
|
||
case LOAD_RECOMMENDATIONS: { | ||
const { fallbackReason, guid, outcome } = action.payload; | ||
|
||
const addons = action.payload.addons | ||
.map((addon) => createInternalAddon(addon)); | ||
|
||
return { | ||
...state, | ||
byGuid: { | ||
...state.byGuid, | ||
[guid]: { | ||
addons, | ||
fallbackReason, | ||
loading: false, | ||
outcome, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
export default reducer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* @flow */ | ||
import { call, put, select, takeLatest } from 'redux-saga/effects'; | ||
import { | ||
FETCH_RECOMMENDATIONS, | ||
abortFetchRecommendations, | ||
loadRecommendations, | ||
} from 'amo/reducers/recommendations'; | ||
import * as api from 'amo/api/recommendations'; | ||
import log from 'core/logger'; | ||
import { createErrorHandler, getState } from 'core/sagas/utils'; | ||
import type { GetRecommendationsParams } from 'amo/api/recommendations'; | ||
import type { | ||
FetchRecommendationsAction, | ||
} from 'amo/reducers/recommendations'; | ||
|
||
|
||
export function* fetchRecommendations({ | ||
payload: { errorHandlerId, guid, recommended }, | ||
}: FetchRecommendationsAction): Generator<any, any, any> { | ||
const errorHandler = createErrorHandler(errorHandlerId); | ||
yield put(errorHandler.createClearingAction()); | ||
|
||
try { | ||
const state = yield select(getState); | ||
|
||
const params: GetRecommendationsParams = { | ||
api: state.api, guid, recommended, | ||
}; | ||
const recommendations = yield call(api.getRecommendations, params); | ||
const { fallback_reason: fallbackReason, outcome, results: addons } | ||
= recommendations; | ||
|
||
yield put(loadRecommendations({ | ||
addons, | ||
fallbackReason, | ||
guid, | ||
outcome, | ||
})); | ||
} catch (error) { | ||
log.warn(`Failed to recommendations: ${error}`); | ||
yield put(errorHandler.createErrorAction(error)); | ||
yield put(abortFetchRecommendations({ guid })); | ||
} | ||
} | ||
|
||
export default function* recommendationsSaga(): Generator<any, any, any> { | ||
yield takeLatest(FETCH_RECOMMENDATIONS, fetchRecommendations); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as api from 'core/api'; | ||
import { getRecommendations } from 'amo/api/recommendations'; | ||
import { createApiResponse } from 'tests/unit/helpers'; | ||
import { dispatchClientMetadata } from 'tests/unit/amo/helpers'; | ||
|
||
|
||
describe(__filename, () => { | ||
it('calls the recommendations API', async () => { | ||
const mockApi = sinon.mock(api); | ||
const apiState = dispatchClientMetadata().store.getState().api; | ||
|
||
const params = { | ||
guid: 'addon-guid', | ||
recommended: true, | ||
}; | ||
|
||
mockApi | ||
.expects('callApi') | ||
.withArgs({ | ||
auth: true, | ||
endpoint: | ||
'addons/recommendations/', | ||
params, | ||
state: apiState, | ||
}) | ||
.once() | ||
.returns(createApiResponse()); | ||
|
||
await getRecommendations({ | ||
api: apiState, | ||
...params, | ||
}); | ||
mockApi.verify(); | ||
}); | ||
}); |
Oops, something went wrong.