diff --git a/calendar/Readme.md b/calendar/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/calendar/escalendar.ecf b/calendar/escalendar.ecf new file mode 100644 index 0000000..a8b847d --- /dev/null +++ b/calendar/escalendar.ecf @@ -0,0 +1,30 @@ + + + + + + /CVS$ + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + + + + + + + + diff --git a/calendar/src/calendar_date.e b/calendar/src/calendar_date.e new file mode 100644 index 0000000..a19d889 --- /dev/null +++ b/calendar/src/calendar_date.e @@ -0,0 +1,35 @@ +note + description: "Summary description for {CALENDAR_DATE}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CALENDAR_DATE + + create + make + + feature + + make(d:DATE; dt: DATE_TIME; tz: STRING) +local + tools :DATE_TIME_TOOLS + do + a_date := d + + + + + + a_date_time := dt + a_time_zone := tz + + end + + a_date:DATE + a_date_time : DATE_TIME + a_time_zone : STRING + + +end diff --git a/calendar/src/calendar_date_payload.e b/calendar/src/calendar_date_payload.e new file mode 100644 index 0000000..c9c9f98 --- /dev/null +++ b/calendar/src/calendar_date_payload.e @@ -0,0 +1,102 @@ +note + description: "Summary description for {CALENDAR_START_PAYLOAD}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CALENDAR_DATE_PAYLOAD + + inherit + JSON_SERIALIZABLE + undefine + default_create + redefine + json_out, + eiffel_date_to_json_string, + eiffel_date_time_to_json_string + end + +create + make_with_date, + default_create + + +feature + +make_with_date(d: CALENDAR_DATE) +do + date := d.a_date + datetime := d.a_date_time + timezone := d.a_time_zone + end + + +default_create +do + create date.make_now + create datetime.make_now + create timezone.make_empty + end + + date: DATE + -- What is the `date' of the Event? + attribute + create Result.make_now + end + + datetime: DATE_TIME + -- What is the `datetime' of the Event? + attribute + create Result.make_now + end + + timeZone: STRING + -- What `timezone' is the Event in? + attribute + create Result.make_empty + end +feature -- Implementation + + eiffel_date_to_json_string (a_key: STRING; a_date: DATE): JSON_STRING + -- Convert `a_date' to JSON_STRING with `a_key' + do + create Result.make_from_string_32 (a_date.formatted_out ("YYYY-[0]MM-[0]DD")) + end + + + eiffel_date_time_to_json_string (a_key: STRING; a_date_time: DATE_TIME): JSON_STRING + -- Convert `a_date_time' to JSON_STRING with `a_key' + do + create Result.make_from_string_32 (a_date_time.formatted_out ("YYYY-[0]MM-[0]DD") + "T" + a_date_time.formatted_out ("[0]hh:[0]mi")+ ":00") + end + + + + json_out: STRING + -- + -- Convert `end_event' to "end", `datetime' to "dateTime", `timezone' to "timeZone" per Google. + do + Result := Precursor + Result.replace_substring_all ("ending", "end") + Result.replace_substring_all ("datetime", "dateTime") + Result.replace_substring_all ("timezone", "timeZone") + end + + metadata_refreshed (a_current: ANY): ARRAY [JSON_METADATA] + do + Result := << + create {JSON_METADATA}.make_text_default + >> + end + + convertible_features (a_object: ANY): ARRAY [STRING] + -- + once + Result := <<"datetime","timezone">> +-- Result := <<"date","dateTime","timeZone">> + end + + + +end diff --git a/calendar/src/calendar_event.e b/calendar/src/calendar_event.e new file mode 100644 index 0000000..f990d32 --- /dev/null +++ b/calendar/src/calendar_event.e @@ -0,0 +1,42 @@ +note + description: "Summary description for {CALENDAR_EVENT}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CALENDAR_EVENT + + create + make_generate_id, + make + + feature + + + make(start_date, end_date : CALENDAR_DATE; sum, event_id : STRING) + do + sd := start_date + ed := end_date + id := event_id + summary := sum + end + + + make_generate_id(start_date, end_date : CALENDAR_DATE) + do + sd := start_date + ed := end_date + id := "create unique id" + summary := "summary" + end + + sd:CALENDAR_DATE + ed:CALENDAR_DATE + id : STRING + summary : STRING + + + + +end diff --git a/calendar/src/calendar_event_payload.e b/calendar/src/calendar_event_payload.e new file mode 100644 index 0000000..59c734f --- /dev/null +++ b/calendar/src/calendar_event_payload.e @@ -0,0 +1,92 @@ +note + description: "Summary description for {CALENDAR_EVENT_PAYLOD}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CALENDAR_EVENT_PAYLOAD + + inherit + JSON_SERIALIZABLE + undefine + default_create + redefine + json_out + end + +create + make, + default_create + + +feature + +make (ce: CALENDAR_EVENT ) +do + kind:= "calendar#event" + summary := ce.summary + create start.make_with_date (ce.sd) + create ending.make_with_date (ce.ed) + id := ce.id +end + +default_create +do + id := "" + kind:= "" + summary:="" + create start + create ending +end + + id : STRING + kind: STRING + summary: STRING + start: CALENDAR_DATE_PAYLOAD + ending: CALENDAR_DATE_PAYLOAD + + + +feature {NONE} -- Implementation: Representation Constants + +-- current_representation: STRING +-- do +-- Result := "{" + +-- "%"start%":%"" + start + "%"," + +-- "%"end%":%"" + ending + "%"" + +-- "}" + +-- end + +feature -- Implementation: Mock Features + + + + +feature -- Implementation + json_out: STRING + -- + -- Convert `end_event' to "end", `datetime' to "dateTime", `timezone' to "timeZone" per Google. + do + Result := Precursor + Result.replace_substring_all ("ending", "end") + Result.replace_substring_all ("datetime", "dateTime") + Result.replace_substring_all ("timezone", "timeZone") + end + + + metadata_refreshed (a_current: ANY): ARRAY [JSON_METADATA] + do + Result := << + create {JSON_METADATA}.make_text_default + >> + end + + convertible_features (a_object: ANY): ARRAY [STRING] + -- + once + Result := <<"summary","kind","start", "ending","id">> + end + +end diff --git a/calendar/src/eg_calendar_api.e b/calendar/src/eg_calendar_api.e new file mode 100644 index 0000000..75ffc3f --- /dev/null +++ b/calendar/src/eg_calendar_api.e @@ -0,0 +1,179 @@ +note + description: "Summary description for {EG_CALENDAR_API}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + EG_CALENDAR_API + +inherit + EG_COMMON_API + +create + make + +feature {NONE} -- Initialization + + make (a_access_token: READABLE_STRING_32) + do + -- Using a code verifier + access_token := a_access_token + enable_version_3 + default_scope + end + + default_scope + do + create {ARRAYED_LIST [STRING_8]} scopes.make (5) + add_scope ("https://www.googleapis.com/auth/calendar") + end + + enable_version_3 + -- Enable Google Calendar version v3. + do + version := "v3" + ensure + version_set: version.same_string ("v3") + end + +feature -- Access + + list_calendars: detachable STRING + do + api_get_call (calendar_url("users/me/calendarList", Void), Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + list_primary_calendar: detachable STRING + do + api_get_call (calendar_url("calendars/primary", Void), Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + list_primary_calendar_events: detachable STRING + do + api_get_call (calendar_url ("calendars/primary/events", Void), Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + create_calendar (name_of_calendar: STRING): detachable STRING + do + api_post_call (calendar_url ("calendars", Void), Void, payload_create_calendar(name_of_calendar), Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + create_calendar_event (name_of_calendar: STRING; payload: CALENDAR_EVENT_PAYLOAD): detachable STRING + require + start_date_exists: attached payload.start + ending_date_exists: attached payload.ending + + do + api_post_call (calendar_url("calendars/" + name_of_calendar + "/events", Void), Void, payload.json_out, Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + check_event_id (id: STRING) : BOOLEAN +-- TODO Create a CALENDAR_EVENT_ID to handle the rules. + +-- Google doc: Provided IDs must follow these rules: +-- - characters allowed in the ID are those used in base32hex encoding, i.e. lowercase letters a-v and digits 0-9, see section 3.1.2 in RFC2938 +-- - the length of the ID must be between 5 and 1024 characters +-- - the ID must be unique per calendar + + do + Result := true + if id.count <= 5 or id.count >= 1024 then + Result := false + elseif not id.as_lower.is_equal (id) then + Result := false + end + + end + + + update_calendar_event (name_of_calendar: STRING; event_id: STRING; payload: CALENDAR_EVENT_PAYLOAD): detachable STRING + require + start_date_exists: attached payload.start + ending_date_exists: attached payload.ending + event_id_follow_google_rules : check_event_id(event_id) + + do + api_put_call (calendar_url("calendars/" + name_of_calendar + "/events/" + event_id, Void), Void, payload.json_out, Void) + if + attached last_response as l_response and then + attached l_response.body as l_body + then + Result := l_body + end + end + + + +feature -- Calenader URL + + calendar_url (a_query: STRING; a_params: detachable STRING): STRING + -- Calenader url endpoint + --| TODO, check if a_params is really neeed. + note + eis: "name=Calendaer service endpoint", "src=https://developers.google.com/calendar/v3/reference", "protocol=uri" + require + a_query_attached: a_query /= Void + do + create Result.make_from_string (endpoint_url) + Result.append ("/") + Result.append (version) + Result.append ("/") + Result.append (a_query) + if attached a_params then + Result.append_character ('?') + Result.append (a_params) + end + ensure + Result_attached: Result /= Void + end + + +feature -- Access + + endpoint_url: STRING + -- + do + Result := "https://www.googleapis.com/calendar" + end + + payload_create_calendar(name:STRING): STRING + local + l_res: JSON_OBJECT + do + create l_res.make_with_capacity (5) + l_res.put_string (name, "summary") + Result := l_res.representation + end + +end diff --git a/calendar/test/calendar_test_set.e b/calendar/test/calendar_test_set.e new file mode 100644 index 0000000..1ae679b --- /dev/null +++ b/calendar/test/calendar_test_set.e @@ -0,0 +1,142 @@ +note + description: "[ + Eiffel tests that can be executed by testing tool. + ]" + author: "EiffelStudio test wizard" + date: "$Date$" + revision: "$Revision$" + testing: "type/manual" + +class + CALENDAR_TEST_SET + +inherit + EQA_TEST_SET + rename + assert as assert_old + redefine + on_prepare, + on_clean + end + + EQA_COMMONLY_USED_ASSERTIONS + undefine + default_create + end + + +feature {NONE} -- Events + + on_prepare + -- + do +-- assert ("not_implemented", False) + end + + on_clean + -- + do +-- assert ("not_implemented", False) + end + +feature -- Test routines + + test_calendar_event_payload + -- New test routine + local + + calendar_event_p : CALENDAR_EVENT_PAYLOAD + calendar_event : CALENDAR_EVENT + start_date : CALENDAR_DATE + end_date : CALENDAR_DATE + + d: DATE + dt: DATE_TIME + tz : STRING + cd : CALENDAR_DATE + expected_json : STRING + + do + create d.make_now + create dt.make_now + tz := "CET" + create start_date.make (d, dt, tz) + create end_date.make (d, dt, tz) + + + create calendar_event.make (start_date, end_date, "test summary", "testcalendareventpaload123") + create calendar_event_p.make (calendar_event) + +-- Removed date might be temporary expected_json := "{%"start%":{%"date%":%"" + d.formatted_out ("YYYY-[0]MM-[0]DD") + "%",%"dateTime%":%"" + dt.formatted_out ("YYYY-[0]MM-[0]DD") + "T"+ dt.formatted_out ("[0]hh:[0]mi") + + expected_json := "{%"start%":{%"dateTime%":%"" + dt.formatted_out ("YYYY-[0]MM-[0]DD") + "T"+ dt.formatted_out ("[0]hh:[0]mi") + ":00" + + "%",%"timeZone%":%"" + tz + "%"}" + ",%"end%":" + + "{%"dateTime%":%"" + dt.formatted_out ("YYYY-[0]MM-[0]DD") + "T"+ dt.formatted_out ("[0]hh:[0]mi") + ":00" + + "%",%"timeZone%":%"" + tz + "%"}" + ",%"kind%":%"calendar#event%",%"summary%":%"test summary%",%"id%":%"testcalendareventpaload123%"}" + + + assert_strings_equal ("Simple test of attributes", expected_json, calendar_event_p.json_out) + + end + + + test_calendar_date_payload + -- New test routine + local + + calendar_date : CALENDAR_DATE_PAYLOAD + d: DATE + dt: DATE_TIME + tz : STRING + cd : CALENDAR_DATE + expected_json : STRING + do + create d.make_now + create dt.make_now + tz := "CET" + create cd.make (d, dt, tz) + + + create calendar_date.make_with_date (cd) +-- I removed DATE as part of the json. I am not sure that it is necessary but since it is working I will stick to the new implementation +-- expected_json := "{%"date%":%"" + d.formatted_out ("YYYY-[0]MM-[0]DD") + "%",%"dateTime%":%"" + dt.formatted_out ("YYYY-[0]MM-[0]DD") + "T" + dt.formatted_out ("[0]hh:[0]mi") + +-- "%",%"timeZone%":%"" + tz + "%"}" + + + expected_json := "{%"dateTime%":%"" + dt.formatted_out ("YYYY-[0]MM-[0]DD") + "T" + dt.formatted_out ("[0]hh:[0]mi") + ":00" + + "%",%"timeZone%":%"" + tz + "%"}" + + + assert_strings_equal ("Simple test of calendar date", expected_json, calendar_date.json_out) + + end + + test_calendar_event_id + local + calednar_api : EG_CALENDAR_API + do + create calednar_api.make ("DUMMAYSTRING") + + assert_booleans_equal ("Should be a correct id", true, calednar_api.check_event_id ("aaaaaaaaaa")) + assert_booleans_equal ("Too few characters", false, calednar_api.check_event_id ("aaaa5")) + assert_booleans_equal ("Minimum nmber of characters", true, calednar_api.check_event_id ("aaaaa6")) + assert_booleans_equal ("Too many characters", false, calednar_api.check_event_id ( create {STRING}.make_filled ('a', 1024))) + assert_booleans_equal ("Maximum number if characters", true, calednar_api.check_event_id ( create {STRING}.make_filled ('a', 1023))) + + assert_booleans_equal ("Uppercase not allowed", false, calednar_api.check_event_id ("aaaaAaaaaa")) + + end + + +feature {NONE} + +simple_start_end_json : STRING = "[ +{"start":"test","end":"test"} +]" + +simple_start_json : STRING = "[ +{"start":"test"} +]" + +end + + diff --git a/calendar/test/test.ecf b/calendar/test/test.ecf new file mode 100644 index 0000000..4e9d9da --- /dev/null +++ b/calendar/test/test.ecf @@ -0,0 +1,30 @@ + + + + + + /CVS$ + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + + + + + + + + diff --git a/calendar/test/test_calendar_api.e b/calendar/test/test_calendar_api.e new file mode 100644 index 0000000..5c85cb9 --- /dev/null +++ b/calendar/test/test_calendar_api.e @@ -0,0 +1,403 @@ +note + description: "Summary description for {TEST_SHEETS_WITH_API_KEY}." + date: "$Date$" + revision: "$Revision$" + +class + TEST_CALENDAR_API + +inherit + + APPLICATION_FLOW + + redefine + Token_file_path_s, + google_auth_path_path_s + end + +create + make + +feature -- {NONE} + + make + do + -- TODO improve this code so we can select which integration test we want to run. + logger.write_information ("make-> ======================> Starting application") + + set_from_json_credentials_file_path (create {PATH}.make_from_string (CREDENTIALS_PATH)) + retrieve_access_token + +-- test_list_calendars +-- test_create_calendar_event + test_update_calendar_event + -- test_list_primary_calendar + -- test_list_primary_calendar_events + -- test_list_calendars + + end + +feature -- Tests + + Token_file_path_s: STRING + do + Result := "/home/anders/token.access" + end + + google_auth_path_path_s: STRING + do + Result := "https://www.googleapis.com/auth/calendar" + end + + test_list_calendars + require + token_is_valid + local + l_esapi: EG_CALENDAR_API + do + -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets + create l_esapi.make (last_token.token) + if attached l_esapi.list_calendars as l_calendars then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_sheet-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_create_the_spreedsheet: False + end + else + check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Listed Calendars%N") + print (l_calendars) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + test_list_primary_calendar + require + token_is_valid + local + l_esapi: EG_CALENDAR_API + do + -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets + create l_esapi.make (last_token.token) + if attached l_esapi.list_primary_calendar as l_calendars then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_sheet-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_create_the_spreedsheet: False + end + else + check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Listed Calendars%N") + print (l_calendars) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + test_list_primary_calendar_events + require + token_is_valid + local + l_esapi: EG_CALENDAR_API + do + -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets + create l_esapi.make (last_token.token) + if attached l_esapi.list_primary_calendar_events as l_calendars then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_sheet-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_create_the_spreedsheet: False + end + else + check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Listed Calendars%N") + print (l_calendars) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + test_create_calendar + require + token_is_valid + local + l_esapi: EG_CALENDAR_API + do + -- https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets + create l_esapi.make (last_token.token) + if attached l_esapi.create_calendar ("BSharpABTODO") as l_calendars then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_calendar-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_create_calwnsar: False + end + else + check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Listed Calendars%N") + print (l_calendars) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + test_create_calendar_event + require + token_is_valid + local + payload: CALENDAR_EVENT_PAYLOAD + l_esapi: EG_CALENDAR_API + ce: CALENDAR_EVENT + start_date, end_date: CALENDAR_DATE + d: DATE + dt: DATE_TIME + de: DATE + dte: DATE_TIME + + do + create d.make_now + create dt.make_now + create start_date.make (d, dt, "Europe/Zurich") + + create de.make_now + create dte.make_now + dte.minute_add (20) + + create end_date.make (de, dte, "Europe/Zurich") + create ce.make (start_date, end_date, "test summary", "createtesteventid123") + create payload.make (ce) + + create l_esapi.make (last_token.token) + if attached l_esapi.create_calendar_event ("primary", payload) as l_calendar_event then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_calendar event-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_create_the_calednar_event: False + end + else +-- check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Created Calednar Event%N") + print (l_calendar_event) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + test_update_calendar_event + require + token_is_valid + local + payload: CALENDAR_EVENT_PAYLOAD + l_esapi: EG_CALENDAR_API + ce: CALENDAR_EVENT + start_date, end_date: CALENDAR_DATE + d: DATE + dt: DATE_TIME + de: DATE + dte: DATE_TIME + + do + create d.make_now + create dt.make_now + create start_date.make (d, dt, "Europe/Zurich") + + create de.make_now + create dte.make_now + dte.minute_add (60) + + create end_date.make (de, dte, "Europe/Zurich") + create ce.make (start_date, end_date, "test summary", "eventidupdatetest123") + create payload.make (ce) + + create l_esapi.make (last_token.token) + if attached l_esapi.update_calendar_event ("primary","testid", payload) as l_calendar_event then + if l_esapi.has_error then + -- debug ("test_create_sheet") + logger.write_error ("test_create_calendar event-> Error") + print ("test_create_sheet-> Error: msg:" + l_esapi.error_message + "%N") + print ("test_create_sheet-> See codes here: https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes") + print ("%N") + -- end + check + cannot_update_the_calednar_event: False + end + else +-- check Json_Field_spreadsheetId: l_calendars.has_substring ("calendar") end + -- check Json_Field_spreadsheetId: l_spreedsheet.has_substring ("spreadsheetId calendarListEntry") end + -- check Json_Field_properties: l_spreedsheet.has_substring ("properties") end + -- check Json_Field_sheets: l_spreedsheet.has_substring ("sheets") end + -- check Json_Field_spreadsheetUrl: l_spreedsheet.has_substring ("spreadsheetUrl") end + -- developerMetadata and namedRanges are optional. + -- debug ("test_create_sheet") + print ("Created Calednar Event%N") + print (l_calendar_event) + print ("%N") + -- end + end + else + -- Bad scope. no connection, etc + check Unexptected_Behavior: False end + end + end + + + + +feature {NONE} -- Implementations + + CREDENTIALS_PATH: STRING = "/home/anders/credentials.json" -- get this file from https://console.developers.google.com/ +-- CREDENTIALS_PATH: STRING = "credentials.json" -- get this file from https://console.developers.google.com/ + -- Credentials path to json file. + +-- impl_append_post_data_sample: STRING +-- local +-- l_res: JSON_OBJECT +-- l_jsa_main, +-- l_jsa_line: JSON_ARRAY +-- j_array: JSON_ARRAY + +-- --{ +-- -- "range": string, +-- -- "majorDimension": enum (Dimension), +-- -- "values": [ +-- -- array +-- -- ] +-- --} +-- --// "values": [ +-- -- // [ +-- -- // "Item", +-- -- // "Cost" +-- -- // ], +-- -- // [ +-- -- // "Wheel", +-- -- // "$20.50" +-- -- // ], +-- -- // [ +-- -- // "Door", +-- -- // "$15" +-- -- // ], +-- -- // [ +-- -- // "Engine", +-- -- // "$100" +-- -- // ], +-- -- // [ +-- -- // "Totals", +-- -- // "$135.50" +-- -- // ] +-- -- // ] + +-- do +-- create l_res.make_with_capacity (5) +-- l_res.put_string ("Sheet1!A1:B5", "range") +-- l_res.put_string ("ROWS", "majorDimension") -- "DIMENSION_UNSPECIFIED", "ROWS", "COLUMNS" + +-- create l_jsa_main.make (10) + +-- create j_array.make (1) +-- create l_jsa_line.make (2) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Item")) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Cost")) +-- j_array.add (l_jsa_line) + +-- create l_jsa_line.make (2) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Wheel")) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("$20.50")) +-- j_array.add (l_jsa_line) + +-- create l_jsa_line.make (2) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Door")) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("$15")) +-- j_array.add (l_jsa_line) + +-- create l_jsa_line.make (2) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Engine")) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("$100")) +-- j_array.add (l_jsa_line) + +-- create l_jsa_line.make (2) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("Totals")) +-- l_jsa_line.extend (create {JSON_STRING}.make_from_string ("$135.50")) +-- j_array.add (l_jsa_line) + +-- l_res.put (j_array, "values") + +-- Result := l_res.representation +-- logger.write_debug ("impl_append_body-> Result: '" + Result.out + "'") +-- end + +end diff --git a/gsuite_base/Readme.md b/gsuite_base/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/sheets/test/application_flow.e b/gsuite_base/application_flow.e similarity index 61% rename from sheets/test/application_flow.e rename to gsuite_base/application_flow.e index 0d879fd..c5dfc2b 100644 --- a/sheets/test/application_flow.e +++ b/gsuite_base/application_flow.e @@ -19,10 +19,12 @@ feature {NONE} -- Initialization local l_secs: INTEGER do + retrieve_access_token_error_has_occured := false create last_token.make_empty get_token if last_token.token.is_empty then logger.write_warning ("retrieve_access_token-> There is something wrong token is empty from file_path: " + Token_file_path_s) + retrieve_access_token_error_has_occured := true check not_happening: False end @@ -31,13 +33,29 @@ feature {NONE} -- Initialization end end + read_token_from_file + local + file: FILE + token: OAUTH_TOKEN + do + create {PLAIN_TEXT_FILE} file.make_with_name (Token_file_path_s) + if file.exists then + file.open_read + file.read_stream (file.count) + if attached {OAUTH_TOKEN} deserialize (file.last_string) as l_token then + last_token := l_token + end + end + end + + get_token local file: FILE token: OAUTH_TOKEN l_date_file: DATE_TIME l_date_now: DATE_TIME - l_diff: INTEGER_64 + l_diff: INTEGER_64 do create {PLAIN_TEXT_FILE} file.make_with_name (Token_file_path_s) if file.exists then @@ -67,6 +85,78 @@ feature {NONE} -- Initialization last_token := token end + get_authorization_url: STRING + require + attached api_key + attached api_secret + local + google: OAUTH_20_GOOGLE_API + config: OAUTH_CONFIG + file: FILE + do + check + attached api_key as l_api_key + attached api_secret as l_api_secret + then + logger.write_debug ("get_token_from_url-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'") + create Result.make_empty + create config.make_default (l_api_key, l_api_secret) + config.set_callback ("urn:ietf:wg:oauth:2.0:oob") + config.set_scope (google_auth_path_path_s) + create google + my_api_service := google.create_service (config) + logger.write_debug ("%N===Google OAuth Workflow ===%N") + + -- Obtain the Authorization URL + logger.write_debug ("%NFetching the Authorization URL..."); + + if attached my_api_service as api_service then + if attached api_service.authorization_url (empty_token) as lauthorization_url then + logger.write_debug ("%NGot the Authorization URL!%N"); + logger.write_debug ("%NNow go and authorize here:%N"); + Result := lauthorization_url + end + end + end + end + + my_api_service: detachable OAUTH_SERVICE_I + + + set_token( autorization_code : STRING) + require + attached api_key + attached api_secret + attached my_api_service + local + google: OAUTH_20_GOOGLE_API + config: OAUTH_CONFIG +-- api_service: OAUTH_SERVICE_I + file: FILE + do + check + attached api_key as l_api_key + attached api_secret as l_api_secret + then +-- logger.write_debug ("get_token_from_url-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'") + +-- create config.make_default (l_api_key, l_api_secret) +-- config.set_callback ("urn:ietf:wg:oauth:2.0:oob") +-- config.set_scope (google_auth_path_path_s) +-- create google +-- api_service := google.create_service (config) +-- logger.write_debug ("%N===Google OAuth Workflow ===%N") + if attached my_api_service as api_service then + if attached api_service.access_token_post (empty_token, create {OAUTH_VERIFIER}.make (autorization_code)) as access_token then + create {PLAIN_TEXT_FILE} file.make_create_read_write (Token_file_path_s) + file.put_string (serialize (access_token)) + file.flush + file.close + end + end + end + end + get_token_from_url: OAUTH_TOKEN require attached api_key @@ -81,11 +171,11 @@ feature {NONE} -- Initialization attached api_key as l_api_key attached api_secret as l_api_secret then - logger.write_debug ("get_token_from_url-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'" ) + logger.write_debug ("get_token_from_url-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'") create Result.make_empty create config.make_default (l_api_key, l_api_secret) config.set_callback ("urn:ietf:wg:oauth:2.0:oob") - config.set_scope ("https://www.googleapis.com/auth/spreadsheets") + config.set_scope (google_auth_path_path_s) create google api_service := google.create_service (config) logger.write_debug ("%N===Google OAuth Workflow ===%N") @@ -112,10 +202,10 @@ feature {NONE} -- Initialization refresh_access_token (a_token: OAUTH_TOKEN): OAUTH_TOKEN -- https://developers.google.com/identity/protocols/oauth2/limited-input-device#offline - --client_id=your_client_id& - --client_secret=your_client_secret& - --refresh_token=refresh_token& - --grant_type=refresh_token + --client_id=your_client_id& + --client_secret=your_client_secret& + --refresh_token=refresh_token& + --grant_type=refresh_token require attached api_key attached api_secret @@ -130,17 +220,26 @@ feature {NONE} -- Initialization attached api_key as l_api_key attached api_secret as l_api_secret then - logger.write_debug ("refresh_access_token-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'" ) + logger.write_debug ("refresh_access_token-> api_key:'" + l_api_key + "' secret:'" + l_api_secret + "'") create Result.make_empty create google - create request.make ("POST", google.access_token_endpoint ) + create request.make ("POST", google.access_token_endpoint) request.add_body_parameter ("client_id", l_api_key) request.add_body_parameter ("client_secret", l_api_secret) request.add_body_parameter ("refresh_token", if attached a_token.refresh_token as l_token then l_token else "" end) + if attached a_token.refresh_token as l_token then + logger.write_debug ("refresh_access_token-> refresh_token: " + l_token) + end request.add_body_parameter ("grant_type", "refresh_token") if attached request.execute as l_response then + logger.write_debug ("refresh_access_token-> Got Response") if attached l_response.body as l_body then + logger.write_debug ("refresh_access_token-> Response included body" + l_body) if attached {OAUTH_TOKEN} google.access_token_extractor.extract (l_body) as l_access_token then + logger.write_debug ("refresh_access_token-> Updating token") + if attached a_token.refresh_token as l_token then + l_access_token.set_refresh_token (l_token) + end create {PLAIN_TEXT_FILE} file.make_create_read_write (Token_file_path_s) file.put_string (serialize (l_access_token)) Result := l_access_token @@ -154,11 +253,19 @@ feature {NONE} -- Initialization feature -- Access - Token_file_path_s: STRING = "token.access" + Token_file_path_s: STRING + do + Result := "token.access" + end + + google_auth_path_path_s: STRING + do + Result := "https://www.googleapis.com/auth/spreadsheets" + end feature -- Status Setting - set_from_json_credentials_file_path (an_fp: PATH) + set_from_json_credentials_file_path (an_fp: PATH) --TODO if file does not exists it shuold not fail. It should try to create a file with correct information. -- sets api_key and api_secret from given api credentials file path normally provided by google -> https://console.developers.google.com -- create a Create OAuth client ID -> desktop app -> and export it to a json file with download link local @@ -177,9 +284,9 @@ feature -- Status Setting check valid_main_object: attached {JSON_OBJECT} l_json_parser.parsed_json_value as l_main_json_o then + -- Installed check - valid_installed: attached {JSON_OBJECT} l_main_json_o.item ("installed") as l_installed_jso - then + valid_installed: attached {JSON_OBJECT} l_main_json_o.item ("installed") as l_installed_jso then check has_client_id: attached {JSON_STRING} l_installed_jso.item ("client_id") as l_client_id_js_s then @@ -205,6 +312,28 @@ feature -- Status report Result := not last_token.token.is_empty end + has_expired: BOOLEAN + require + valid_token: token_is_valid + local + file: FILE + l_date_file: DATE_TIME + l_date_now: DATE_TIME + l_diff: INTEGER_64 + do + read_token_from_file + create {PLAIN_TEXT_FILE} file.make_with_name (Token_file_path_s) + create l_date_file.make_from_epoch (file.date) + create l_date_now.make_now_utc + l_diff := l_date_now.definite_duration (l_date_file).seconds_count + if l_diff > last_token.expires_in-300 then + Result := True + end + end + + retrieve_access_token_error_has_occured : BOOLEAN + + feature -- Serialize Access Token serialize (a_object: ANY): STRING @@ -260,4 +389,5 @@ feature {NONE} -- Implementation api_key: detachable STRING api_secret: detachable STRING empty_token: detachable OAUTH_TOKEN + end diff --git a/gsuite_base/eg_base.ecf b/gsuite_base/eg_base.ecf new file mode 100644 index 0000000..1adaa04 --- /dev/null +++ b/gsuite_base/eg_base.ecf @@ -0,0 +1,31 @@ + + + + + + /CVS$ + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + + + + + + + + + diff --git a/gsuite_base/eg_common_api.e b/gsuite_base/eg_common_api.e new file mode 100644 index 0000000..a686302 --- /dev/null +++ b/gsuite_base/eg_common_api.e @@ -0,0 +1,370 @@ +note + description: "Common abstraction to access (read/write Google API data)" + date: "$Date$" + revision: "$Revision$" + +deferred class + EG_COMMON_API + +inherit + + LOGGABLE + +feature -- Reset + + reset + do + create access_token.make_empty + end + +feature -- Access + + access_token: STRING_32 + -- Google OAuth2 access token. + + http_status: INTEGER + -- Contains the last HTTP status code returned. + + last_api_call: detachable STRING + -- Contains the last API call. + + last_response: detachable OAUTH_RESPONSE + -- Cointains the last API response. + + version: STRING_8 + -- Google API version. + + scopes: LIST [STRING_8] + --Lists of Authorization Scopes. + +feature -- Scopes + + add_scope (a_scope: STRING_8) + -- Add an scope `a_scope` to the list of scopes. + do + scopes.force (a_scope) + end + + clear_scopes + -- Remove all items. + do + scopes.wipe_out + ensure + scopes.is_empty + end + +feature -- Query Parameters Factory + + query_parameters (a_params: detachable STRING_TABLE [STRING] ): detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]] + -- @JV please add a call example + local + l_result: like query_parameters + l_tuple : like query_parameters.item + i: INTEGER + do + if attached a_params then + i := 1 + create l_result.make_filled (Void, 1, a_params.count) + across a_params as ic + loop + create l_tuple.default_create + l_tuple.put (ic.key, 1) + l_tuple.put (ic.item, 2) + l_result.force (l_tuple, i) + i := i + 1 + end + Result := l_result + end + end + +feature -- Error Report + + parse_last_response + require + attached last_response + local + l_json_parser: JSON_PARSER + do + check + attached last_response as l_response + then + if l_response.status = {HTTP_STATUS_CODE}.unauthorized then + logger.write_error ("parse_last_response->Unauthorized status, review your authorization credentials") + end + if attached l_response.body as l_body then + logger.write_debug ("parse_last_response->body:" + l_body) + create l_json_parser.make_with_string (l_body) + l_json_parser.parse_content + if l_json_parser.is_valid then + if attached {JSON_OBJECT} l_json_parser.parsed_json_value as l_main_jso then + if attached {JSON_OBJECT} l_main_jso.item ("error") as l_error_jso then + if attached {JSON_NUMBER} l_error_jso.item ("code") as l_jso then + print ("parse_last_response-> error code:" + l_jso.representation + "%N") + end + if attached {JSON_STRING} l_error_jso.item ("message") as l_jso then + print ("parse_last_response-> error message:" + l_jso.unescaped_string_8 + "%N") + end + if attached {JSON_STRING} l_error_jso.item ("status") as l_jso then + print ("parse_last_response-> error status:" + l_jso.unescaped_string_8 + "%N") + end + end + end + else + print ("parse_last_response-> Error: Invalid json body content:" + l_body + "%N") + end + end + end + end + + has_error: BOOLEAN + -- Last api call raise an error? + do + if attached last_response as l_response then + Result := l_response.status /= 200 + else + Result := False + end + end + + error_message: STRING + -- Last api call error message. + require + has_error: has_error + do + if + attached last_response as l_response + then + if + attached l_response.error_message as l_error_message + then + Result := l_error_message + else + Result := l_response.status.out + end + else + Result := "Unknown Error" + end + end + +feature {NONE} -- Implementation + + api_post_call (a_api_url: STRING; a_query_params: detachable STRING_TABLE [STRING]; a_payload: detachable STRING; a_upload_data: detachable TUPLE[data:PATH; content_type: STRING]) + note + eis: "name=payload_body", "src=https://tools.ietf.org/html/draft-ietf-httpbis-p3-payload-14#section-3.2", "protocol=https://tools.ietf.org/html/draft-ietf-httpbis-p3-payload-14#section-3.2" + -- POST REST API call for `a_api_url' + do + internal_api_call (a_api_url, "POST", a_query_params, a_payload, a_upload_data) + end + + api_put_call (a_api_url: STRING; a_query_params: detachable STRING_TABLE [STRING]; a_payload: detachable STRING; a_upload_data: detachable TUPLE[data:PATH; content_type: STRING]) + note + eis: "name=payload_body", "src=https://tools.ietf.org/html/draft-ietf-httpbis-p3-payload-14#section-3.2", "protocol=https://tools.ietf.org/html/draft-ietf-httpbis-p3-payload-14#section-3.2" + -- POST REST API call for `a_api_url' + do + internal_api_call (a_api_url, "PUT", a_query_params, a_payload, a_upload_data) + end + + + api_delete_call (a_api_url: STRING; a_query_params: detachable STRING_TABLE [STRING]) + -- DELETE REST API call for `a_api_url' + do + internal_api_call (a_api_url, "DELETE", a_query_params, Void, Void) + end + + api_get_call (a_api_url: STRING; a_query_params: detachable STRING_TABLE [STRING]) + -- GET REST API call for `a_api_url' + do + internal_api_call (a_api_url, "GET", a_query_params, Void, Void) + end + + internal_api_call (a_api_url: STRING; a_method: STRING; a_query_params: detachable STRING_TABLE [STRING]; a_payload: detachable STRING; a_upload_data: detachable TUPLE[filename:PATH; content_type: STRING]) + note + EIS:"name=access token", "src=https://developers.google.com/identity/protocols/oauth2", "protocol=uri" + local + request: OAUTH_REQUEST + l_access_token: detachable OAUTH_TOKEN + api_service: OAUTH_20_SERVICE + config: OAUTH_CONFIG + do + logger.write_debug ("internal_api_call-> a_api_url:" + a_api_url + " method:" + a_method) + -- TODO improve this, so we can check the required scopes before we + -- do an api call. + -- TODO add a class with the valid scopes. + create config.make_default ("", "") + + -- Add scopes + if scopes.is_empty then + -- Don't add scopes if the list is empty. + else + config.set_scope (build_scope) + end + + -- Initialization + + create api_service.make (create {OAUTH_20_GOOGLE_API}, config) + --| TODO improve cypress service creation procedure to make configuration optional. + + --| TODO rewrite prints as logs + logger.write_debug ("%N===Google OAuth Workflow using OAuth access token for the owner of the application ===%N") + + -- Create the access token that will identifies the user making the request. + create l_access_token.make_token_secret (access_token, "NOT_NEEDED") + --| Todo improve access_token to create a token without a secret. + check attached l_access_token as ll_access_token then + logger.write_information ("internal_api_call->Got the Access Token:" + ll_access_token.token); + + --Now let's go and check if the request is signed correcty + logger.write_information ("internal_api_call->Now we're going to verify our credentials...%N"); + -- Build the request and authorize it with OAuth. + create request.make (a_method, a_api_url) + + -- Workaorund to make it work with Google API + -- in other case it return HTTP 411 Length Required. + -- https://tools.ietf.org/html/rfc7231#section-6.5.10 + -- + -- + -- https://tools.ietf.org/html/rfc7230#section-3.3.2 + -- + upload_data (a_method, request, a_upload_data) + add_parameters (a_method, request, a_query_params) + -- adding payload + if attached a_payload then + request.add_header ("Content-length", a_payload.count.out) + request.add_header ("Content-Type", "application/json; charset=UTF-8") + request.add_payload (a_payload) + logger.write_debug ("internal_api_call->payload:'" + a_payload + "'") + + else + request.add_header ("Content-length", "0") + end + + api_service.sign_request (ll_access_token, request) + + logger.write_debug ("internal_api_call->uri:'" + request.uri + "'") + if attached request.upload_file as l_s then + logger.write_debug ("internal_api_call->upload file:'" + l_s.out + "'") + end + if attached {OAUTH_RESPONSE} request.execute as l_response then + last_response := l_response + end + end + last_api_call := a_api_url.string + end + + + url (a_base_url: STRING; a_parameters: detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]]): STRING + -- url for `a_base_url' and `a_parameters' + require + a_base_url_attached: a_base_url /= Void + do + create Result.make_from_string (a_base_url) + append_parameters_to_url (Result, a_parameters) + end + + append_parameters_to_url (a_url: STRING; a_parameters: detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]]) + -- Append parameters `a_parameters' to `a_url' + require + a_url_attached: a_url /= Void + local + i: INTEGER + l_first_param: BOOLEAN + do + if a_parameters /= Void and then a_parameters.count > 0 then + if a_url.index_of ('?', 1) > 0 then + l_first_param := False + elseif a_url.index_of ('&', 1) > 0 then + l_first_param := False + else + l_first_param := True + end + from + i := a_parameters.lower + until + i > a_parameters.upper + loop + if attached a_parameters[i] as a_param then + if l_first_param then + a_url.append_character ('?') + else + a_url.append_character ('&') + end + a_url.append_string (a_param.name) + a_url.append_character ('=') + a_url.append_string (a_param.value) + l_first_param := False + end + i := i + 1 + end + end + end + + add_parameters (a_method: STRING; request: OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) + -- add parameters 'a_params' (with key, value) to the oauth request 'request'. + --| at the moment all params are added to the query_string. + do + --| TODO check if we need to call add_body_parameters if the method is PUT or POST + add_query_parameters (request, a_params) + end + + add_query_parameters (request:OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) + do + if attached a_params then + across a_params as ic + loop + request.add_query_string_parameter (ic.key.as_string_8, ic.item) + end + end + end + + add_body_parameters (request:OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) + do + if attached a_params then + across a_params as ic + loop + request.add_body_parameter (ic.key.as_string_8, ic.item) + end + end + end + + upload_data (a_method: STRING; request:OAUTH_REQUEST; a_upload_data: detachable TUPLE[file_name:PATH; content_type: STRING]) + local + l_raw_file: RAW_FILE + do + if a_method.is_case_insensitive_equal_general ("POST") and then attached a_upload_data as l_upload_data then + create l_raw_file.make_open_read (l_upload_data.file_name.absolute_path.name) + if l_raw_file.exists then + logger.write_debug ("upload_data-> Content-type: '" + l_upload_data.content_type + "'") + logger.write_debug ("upload_data-> upload file name: '" + l_upload_data.file_name.absolute_path.name.out + "'") + request.add_header ("Content-Type", l_upload_data.content_type) + request.set_upload_filename (l_upload_data.file_name.absolute_path.name) + request.add_form_parameter("source", l_upload_data.file_name.name.as_string_32) + end + end + end + + + build_scope: STRING + -- Create an string as list of space- delimited, case-sensitive strings. + -- https://tools.ietf.org/html/rfc6749#section-3.3 + do + if scopes.is_empty then + Result := "" + else + Result := "" + across scopes as ic loop + Result.append (ic.item) + Result.append_character (' ') + end + Result.adjust + end + end + +feature -- Service Endpoint + + endpoint_url: STRING + -- base URL that specifies the network address of an API service. + deferred + end + +end + diff --git a/sheets/src/logger/loggable.e b/gsuite_base/logger/loggable.e similarity index 100% rename from sheets/src/logger/loggable.e rename to gsuite_base/logger/loggable.e diff --git a/sheets/esheets.ecf b/sheets/esheets.ecf index ddf095d..a2c5b8d 100644 --- a/sheets/esheets.ecf +++ b/sheets/esheets.ecf @@ -18,13 +18,11 @@ - - - - + + diff --git a/sheets/src/eg_sheets_api.e b/sheets/src/eg_sheets_api.e index 3379197..d8ddef4 100644 --- a/sheets/src/eg_sheets_api.e +++ b/sheets/src/eg_sheets_api.e @@ -5,8 +5,13 @@ note class EG_SHEETS_API + inherit - LOGGABLE + + EG_COMMON_API + rename + endpoint_url as endpoint_sheets_url + end create make @@ -18,33 +23,17 @@ feature {NONE} -- Initialization -- Using a code verifier access_token := a_access_token enable_version_4 + default_scope end -feature -- Reset - - reset + default_scope do - create access_token.make_empty + create {ARRAYED_LIST [STRING_8]} scopes.make (5) + add_scope ("https://www.googleapis.com/auth/spreadsheets") end -feature -- Access - - access_token: STRING_32 - -- Google OAuth2 access token. - - http_status: INTEGER - -- Contains the last HTTP status code returned. - - last_api_call: detachable STRING - -- Contains the last API call. - - last_response: detachable OAUTH_RESPONSE - -- Cointains the last API response. - - version: STRING_8 - -- Google Sheets version - feature -- Spreedsheets + spreadsheet_id: detachable STRING @@ -62,7 +51,7 @@ feature -- Spreedsheets Operations then Result := l_body end - end + end get_from_id (a_spreadsheet_id: attached like spreadsheet_id; a_params: detachable EG_SPREADSHEET_PARAMETERS): detachable like last_response.body -- POST /spreadsheets/`a_spreadsheet_id' @@ -211,6 +200,28 @@ feature -- Spreedsheets Operations end end +feature -- SpreedSheet URL + + sheets_url (a_query: STRING; a_params: detachable STRING): STRING + -- Sheet url endpoint + note + eis: "name=Sheets service endpoint", "src=https://developers.google.com/sheets/api/reference/rest", "protocol=uri" + require + a_query_attached: a_query /= Void + do + create Result.make_from_string (endpoint_sheets_url) + Result.append ("/") + Result.append (version) + Result.append ("/") + Result.append (a_query) + if attached a_params then + Result.append_character ('?') + Result.append (a_params) + end + ensure + Result_attached: Result /= Void + end + feature -- Parameters Factory parameters (a_params: detachable STRING_TABLE [STRING] ): detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]] @@ -235,75 +246,6 @@ feature -- Parameters Factory end end -feature -- Error Report - - parse_last_response - require - attached last_response - local - l_json_parser: JSON_PARSER - do - check - attached last_response as l_response - then - if l_response.status = {HTTP_STATUS_CODE}.unauthorized then - logger.write_error ("parse_last_response->Unauthorized status, review your authorization credentials") - end - if attached l_response.body as l_body then - logger.write_debug ("parse_last_response->body:" + l_body) - create l_json_parser.make_with_string (l_body) - l_json_parser.parse_content - if l_json_parser.is_valid then - if attached {JSON_OBJECT} l_json_parser.parsed_json_value as l_main_jso then - if attached {JSON_OBJECT} l_main_jso.item ("error") as l_error_jso then - if attached {JSON_NUMBER} l_error_jso.item ("code") as l_jso then - print ("parse_last_response-> error code:" + l_jso.representation + "%N") - end - if attached {JSON_STRING} l_error_jso.item ("message") as l_jso then - print ("parse_last_response-> error message:" + l_jso.unescaped_string_8 + "%N") - end - if attached {JSON_STRING} l_error_jso.item ("status") as l_jso then - print ("parse_last_response-> error status:" + l_jso.unescaped_string_8 + "%N") - end - end - end - else - print ("parse_last_response-> Error: Invalid json body content:" + l_body + "%N") - end - end - end - end - - has_error: BOOLEAN - -- Last api call raise an error? - do - if attached last_response as l_response then - Result := l_response.status /= 200 - else - Result := False - end - end - - error_message: STRING - -- Last api call error message. - require - has_error: has_error - do - if - attached last_response as l_response - then - if - attached l_response.error_message as l_error_message - then - Result := l_error_message - else - Result := l_response.status.out - end - else - Result := "Unknown Error" - end - end - feature -- Versions enable_version_4 @@ -314,201 +256,15 @@ feature -- Versions version_set: version.same_string ("v4") end -feature {NONE} -- Implementation - - api_post_call (a_api_url: STRING; a_params: detachable STRING_TABLE [STRING]; a_payload: detachable STRING; a_upload_data: detachable TUPLE[data:PATH; content_type: STRING]) - -- POST REST API call for `a_api_url' - do - internal_api_call (a_api_url, "POST", a_params, a_payload, a_upload_data) - end - - api_delete_call (a_api_url: STRING; a_params: detachable STRING_TABLE [STRING]) - -- DELETE REST API call for `a_api_url' - do - internal_api_call (a_api_url, "DELETE", a_params, Void, Void) - end - - api_get_call (a_api_url: STRING; a_params: detachable STRING_TABLE [STRING]) - -- GET REST API call for `a_api_url' - do - internal_api_call (a_api_url, "GET", a_params, Void, Void) - end - - internal_api_call (a_api_url: STRING; a_method: STRING; a_params: detachable STRING_TABLE [STRING]; a_payload: detachable STRING; a_upload_data: detachable TUPLE[filename:PATH; content_type: STRING]) - note - EIS:"name=access token", "src=https://developers.google.com/identity/protocols/oauth2", "protocol=uri" - local - request: OAUTH_REQUEST - l_access_token: detachable OAUTH_TOKEN - api_service: OAUTH_20_SERVICE - config: OAUTH_CONFIG - do - logger.write_debug ("internal_api_call-> a_api_url:" + a_api_url + " method:" + a_method) - -- TODO improve this, so we can check the required scopes before we - -- do an api call. - -- TODO add a class with the valid scopes. - create config.make_default ("", "") - config.set_scope ("https://www.googleapis.com/auth/spreadsheets") - - -- Initialization - - create api_service.make (create {OAUTH_20_GOOGLE_API}, config) - --| TODO improve cypress service creation procedure to make configuration optional. - - --| TODO rewrite prints as logs - logger.write_debug ("%N===Google OAuth Workflow using OAuth access token for the owner of the application ===%N") - - -- Create the access token that will identifies the user making the request. - create l_access_token.make_token_secret (access_token, "NOT_NEEDED") - --| Todo improve access_token to create a token without a secret. - check attached l_access_token as ll_access_token then - logger.write_information ("internal_api_call->Got the Access Token:" + ll_access_token.token); - - --Now let's go and check if the request is signed correcty - logger.write_information ("internal_api_call->Now we're going to verify our credentials...%N"); - -- Build the request and authorize it with OAuth. - create request.make (a_method, a_api_url) - -- Workaorund to make it work with Google API - -- in other case it return HTTP 411 Length Required. - -- Todo check. - upload_data (a_method, request, a_upload_data) - add_parameters (a_method, request, a_params) - -- adding payload - if attached a_payload then - request.add_header ("Content-length", a_payload.count.out) - request.add_header ("Content-Type", "application/json; charset=UTF-8") - request.add_payload (a_payload) - else - request.add_header ("Content-length", "0") - end - - api_service.sign_request (ll_access_token, request) - - logger.write_debug ("internal_api_call->uri:'" + request.uri + "'") - if attached request.upload_file as l_s then - logger.write_debug ("internal_api_call->upload file:'" + l_s.out + "'") - end - if attached {OAUTH_RESPONSE} request.execute as l_response then - last_response := l_response - end - end - last_api_call := a_api_url.string - end - - sheets_url (a_query: STRING; a_params: detachable STRING): STRING - -- Sheet url endpoint - note - eis: "name=Sheets service endpoint", "src=https://developers.google.com/sheets/api/reference/rest", "protocol=uri" - require - a_query_attached: a_query /= Void - do - create Result.make_from_string (endpooint_sheets_url) - Result.append ("/") - Result.append (version) - Result.append ("/") - Result.append (a_query) - if attached a_params then - Result.append_character ('?') - Result.append (a_params) - end - ensure - Result_attached: Result /= Void - end - - url (a_base_url: STRING; a_parameters: detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]]): STRING - -- url for `a_base_url' and `a_parameters' - require - a_base_url_attached: a_base_url /= Void - do - create Result.make_from_string (a_base_url) - append_parameters_to_url (Result, a_parameters) - end - append_parameters_to_url (a_url: STRING; a_parameters: detachable ARRAY [detachable TUPLE [name: STRING; value: STRING]]) - -- Append parameters `a_parameters' to `a_url' - require - a_url_attached: a_url /= Void - local - i: INTEGER - l_first_param: BOOLEAN - do - if a_parameters /= Void and then a_parameters.count > 0 then - if a_url.index_of ('?', 1) > 0 then - l_first_param := False - elseif a_url.index_of ('&', 1) > 0 then - l_first_param := False - else - l_first_param := True - end - from - i := a_parameters.lower - until - i > a_parameters.upper - loop - if attached a_parameters[i] as a_param then - if l_first_param then - a_url.append_character ('?') - else - a_url.append_character ('&') - end - a_url.append_string (a_param.name) - a_url.append_character ('=') - a_url.append_string (a_param.value) - l_first_param := False - end - i := i + 1 - end - end - end - - add_parameters (a_method: STRING; request:OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) - -- add parameters 'a_params' (with key, value) to the oauth request 'request'. - --| at the moment all params are added to the query_string. - do - add_query_parameters (request, a_params) - end - - add_query_parameters (request:OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) - do - if attached a_params then - across a_params as ic - loop - request.add_query_string_parameter (ic.key.as_string_8, ic.item) - end - end - end - - add_body_parameters (request:OAUTH_REQUEST; a_params: detachable STRING_TABLE [STRING]) - do - if attached a_params then - across a_params as ic - loop - request.add_body_parameter (ic.key.as_string_8, ic.item) - end - end - end +feature -- Service Endpoint - upload_data (a_method: STRING; request:OAUTH_REQUEST; a_upload_data: detachable TUPLE[file_name:PATH; content_type: STRING]) - local - l_raw_file: RAW_FILE + endpoint_sheets_url: STRING + -- do - if a_method.is_case_insensitive_equal_general ("POST") and then attached a_upload_data as l_upload_data then - create l_raw_file.make_open_read (l_upload_data.file_name.absolute_path.name) - if l_raw_file.exists then - logger.write_debug ("upload_data-> Content-type: '" + l_upload_data.content_type + "'") - logger.write_debug ("upload_data-> upload file name: '" + l_upload_data.file_name.absolute_path.name.out + "'") - request.add_header ("Content-Type", l_upload_data.content_type) - request.set_upload_filename (l_upload_data.file_name.absolute_path.name) - request.add_form_parameter("source", l_upload_data.file_name.name.as_string_32) - end - end + Result := "https://sheets.googleapis.com" end -feature -- Service Endpoint - - endpooint_sheets_url: STRING = "https://sheets.googleapis.com" - -- base URL that specifies the network address of an API service. - feature {NONE} -- Implementation data_file: detachable PLAIN_TEXT_FILE diff --git a/sheets/test/test.ecf b/sheets/test/test.ecf index 46d1006..743dea7 100644 --- a/sheets/test/test.ecf +++ b/sheets/test/test.ecf @@ -1,5 +1,5 @@ - + /CVS$ @@ -7,7 +7,7 @@ /\.git$ /\.svn$ - @@ -20,11 +20,11 @@ - + - @@ -32,7 +32,7 @@ - diff --git a/sheets/test/test_sheets_api.e b/sheets/test/test_sheets_api.e index 9bf3199..1b3b87e 100644 --- a/sheets/test/test_sheets_api.e +++ b/sheets/test/test_sheets_api.e @@ -10,6 +10,7 @@ inherit APPLICATION_FLOW + create make @@ -20,13 +21,14 @@ feature -- {NONE} -- TODO improve this code so we can select which integration test we want to run. logger.write_information ("make-> ======================> Starting application") set_from_json_credentials_file_path (create {PATH}.make_from_string (credentials_path)) + retrieve_access_token test_create_sheet ---- test_get_sheet ("1v1N4nRa6mmLcP9rUuyQPiCnLuUcBQFDEC7E0CDg3ASI") --- test_append_sheet ("19cKCmQBWJoMePX0Iy6LueHRw0sS2bMcyP1Auzbkvj6M", impl_append_post_data_sample) --pg + test_append_sheet ("19cKCmQBWJoMePX0Iy6LueHRw0sS2bMcyP1Auzbkvj6M", impl_append_post_data_sample) --pg -- --test_append_sheet ("1j5CTkpgOc6Y5qgYdA_klZYjNhmN2KYocoZAdM4Y61tw") --jv ----- set_from_json_credentials_file_path (create {PATH}.make_from_string (CREDENTIALS_PATH)) +---- set_from_json_credentials_file_path (create {PATH}.make_from_string (CREDENTIALS_PATH)) -- retrieve_access_token -- test_get_sheet ("1v1N4nRa6mmLcP9rUuyQPiCnLuUcBQFDEC7E0CDg3ASI") end @@ -34,7 +36,8 @@ feature -- {NONE} feature -- Tests - test_create_sheet + + test_create_sheet require token_is_valid local @@ -151,11 +154,10 @@ feature -- Tests feature {NONE} -- Implementations - CREDENTIALS_PATH: STRING="credentials.json" -- get this file from https://console.developers.google.com/ + CREDENTIALS_PATH: STRING="credentials.json" -- get this file from https://console.developers.google.com/ -- Credentials path to json file. - impl_append_post_data_sample: STRING local l_res: JSON_OBJECT