{"action":"created","comment":{"url":"https://api.github.com/repos/ming13/pages/pulls/comments/213836194","pull_request_review_id":150743731,"id":213836194,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxMzgzNjE5NA==","diff_hunk":"@@ -0,0 +1,294 @@\n+---\n+title: \"Reactive Abstractions in Android World\"\n+description: \"Abstracting and testing platform interactions.\"\n+date: 2018-08-26\n+slug: reactive-abstractions-in-android-world\n+---\n+\n+Who knows how many test suites were not created because of a classic parry.\n+\n+> It cannot be tested — it uses a platform call!\n+\n+Well, it is not actually true all the time.\n+Unit testing something that only a human eye and neural networks\n+can catch — like animations — doesn’t make sense.\n+On the other hand, retrying a network request on a re-established connection\n+can and should be tested. As a bonus, it is possible to gain a couple of perks.\n+\n+# Theory\n+\n+The main advice I can give Android developers about testing —\n+start thinking about the codebase as a platform-agnostic environment.\n+Not in a ridiculous way but in a more pragmatic one — otherwise, it is too\n+easy to slip on a dark cross-platform path. Associating the codebase\n+with the JVM platform and not specifically Android is a better idea.\n+Framework-related interactions can be plugged-in as composable blocks.\n+\n+Another advice — embrace abstractions available on hand.\n+Samples in this article are based on RxJava but it is possible to replace it\n+with `Future`, `Promise` or Kotlin coroutines.\n+This is especially useful with `async` + `await` type of interactions.\n+Believe me, it is not wise to do everything from scratch if there is a good tech\n+available. Moreover, it is easier to maintain consistency\n+in the codebase if each component speaks using the same constructs.\n+Do not repeat the [Tower of Babel](https://en.wikipedia.org/wiki/Tower_of_Babel) fall.\n+\n+# Practice\n+\n+Lke it or not — platform interactions leak into the business logic.\n+It is understandable — the environment capabilities should be utilized and not ignored.\n+\n+At the same time it is essential to test the logic of the final product —\n+otherwise, there will be no product at all, only issues and undefined behavior.\n+\n+Fortunately enough, the mankind deals with such issues for a while.\n+Let’s use a weight measurement example. How much does a brick weight?\n+Well, certainly less than a space station and more than an atom.\n+That characteristic does not really help when it is necessary to transport\n+a number of bricks. Will a car break under a million bricks?\n+That’s why humanity created _abstractions_, such as grams, kilograms and tons.\n+It is possible to take this completely (but collectively) made up measurement unit\n+and apply it everywhere.\n+\n+Software development provides means to create abstractions easy-as.\n+It is not necessary to create\n+an [ISO](https://www.iso.org/) committee to create one — a programming language is enough.\n+Android is not an exception — and it never was.\n+\n+## Connectivity\n+\n+It can be useful to retry stalled network requests when OS reconnects\n+to a network access point. In fact,\n+[`ConnectivityManager`](https://developer.android.com/reference/android/net/ConnectivityManager)\n+was there for centuries and can help with this issue:\n+\n+> The primary responsibilities of this class are to:\n+> monitor network connections (Wi-Fi, GPRS, UMTS, etc)...\n+\n+The usage of the potential abstraction looks like this.\n+\n+```kotlin\n+disposable += connectivity.available.subscribe(refresh)\n+```\n+\n+Seems like a single stream will be enough. Let’s do it!\n+\n+```kotlin\n+interface Connectivity {\n+\n+    val available: Observable<Unit>\n+\n+    class AndroidConnectivity(context: AndroidContext) : Connectivity {\n+\n+        private val manager = context.systemService<ConnectivityManager>()\n+\n+        override val available = Observable.create<Unit> { emitter ->\n+            val listener = OnNetworkActiveListener { emitter.onNext(Unit) }\n+\n+            manager.addDefaultNetworkActiveListener(listener)\n+            emitter.setCancellable { manager.removeDefaultNetworkActiveListener(listener) }\n+        }\n+    }\n+}\n+```\n+\n+Marvelous! A couple of things to notice here.\n+\n+The `Observable` itself handles proper `Listener` setting on subscription and\n+unsetting on unsubscription. It uses the\n+[Dispose pattern](https://en.wikipedia.org/wiki/Dispose_pattern),\n+i. e. the RxJava `Disposable`. Since all `Observable` behave the same\n+the end-user of the `Connectivity` will work with it as with any other\n+`Observable`.\n+\n+Since there is an `interface` it is easy to provide a `Test*` implementation\n+for unit tests. Just a single call to simulate the real-world behavior and that’s it —\n+it is possible to test a code which works with the platform framework.\n+\n+```kotlin\n+class TestConnectivity : Connectivity {\n+    override val available = PublishSubject.create<Unit>()\n+}\n+```\n+\n+```kotlin\n+context(\"connectivity becomes available\") {\n+\n+    beforeEach {\n+        connectivity.available.onNext(Unit)\n+    }\n+\n+    it(\"refreshes\") {\n+        verify(refresh).run()\n+    }\n+}\n+```\n+\n+## Launching\n+\n+What about starting something via `Intent`? The operation itself is trivial,\n+but it is necessary to keep in mind that there might be an `ActivityNotFoundException`.\n+This exception is being thrown when nobody can handle our intention.\n+\n+```kotlin\n+disposable += launcher.launch(Request.ShareText(\"Ping!\"))\n+    .observeOn(mainThread)\n+    .subscribe {\n+        when (result) {\n+            Result.Success -> view.showSuccessAlert()\n+            Result.Failure -> view.showFailureAlert()\n+        }\n+    }\n+```\n+\n+To achieve this we are going to create our own abstraction over `Intent`.\n+Doing so will hide the complexity and will help to avoid a copy-paste\n+of the same `Intent` building over and over again.\n+\n+```kotlin\n+interface Launcher {\n+\n+    sealed class Request {\n+        data class ShareText(val text: String) : Request()\n+    }\n+\n+    enum class Result { Success, Failure }\n+\n+    fun launch(request: Request): Single<Result>\n+\n+    class AndroidLauncher(\n+            private val application: Application,\n+            private val mainScheduler: Scheduler\n+    ) : Launcher {\n+\n+        override fun launch(request: Request) = application.currentActivity\n+                .take(1)\n+                .singleOrError()\n+                .map { activity ->\n+                    try {\n+                        activity.startActivity(createIntent(request))\n+                        Result.Success\n+                    } catch (e: ActivityNotFoundException) {\n+                        Result.Failure\n+                    }\n+                }\n+                .subscribeOn(mainScheduler)\n+\n+        private inline fun createIntent(request: Request) = when (request) {\n+            is Request.ShareText -> {\n+                val intent = Intent(Intent.ACTION_SEND).apply {\n+                    flags = Intent.FLAG_ACTIVITY_NEW_TASK\n+                    type = \"text/plain\"\n+                    putExtra(Intent.EXTRA_TEXT, request.text)\n+                }\n+\n+                Intent.createChooser(intent, /* chooser title */ null)\n+            }\n+        }\n+    }\n+}\n+```\n+\n+We are controlling the thread under the hood so the consumer does not need to know\n+implementation details.\n+\n+An attentive reader might notice that we are using\n+another abstraction — `Application#currentActivity`. It is trivial to implement using\n+[`ActivityLifecycleCallbacks`](https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks) and\n+`Connectivity`-like approach to listeners and callbacks.\n+\n+Of course, we can test the behavior.\n+\n+```kotlin\n+class TestLauncher: Launcher {\n+    val result = SingleSubject.create<Result>()\n+\n+    override fun launch(request: Request) = result\n+}\n+```\n+\n+```kotlin\n+context(\"launch result is success\") {\n+\n+    beforeEach {\n+        launcher.result.onSuccess(Result.Success)\n+    }\n+\n+    it(\"shows success alert\") {\n+        verify(view).showSuccessAlert()\n+    }\n+}\n+\n+context(\"launch result is failure\") {\n+\n+    beforeEach {\n+        launcher.result.onSuccess(Result.Failure)\n+    }\n+\n+    it(\"shows failure alert\") {\n+        verify(view).showFailureAlert()\n+    }\n+}\n+```\n+\n+## Google Play Services\n+\n+The concept works well not only with the platform\n+but with all third-party information sources — such as various SDK.\n+In such cases, it is possible to improve the external API — for example,\n+provide proper nullability handling if the SDK is not annotated with `@Nullable` and `@NonNull`.\n+\n+Let’s say it is necessary to show a warning if Google Play Services\n+is not installed.\n+\n+```kotlin\n+disposable += googlePlayServices.available\n+    .filter { it == false }\n+    .observeOn(mainThread)\n+    .subscribe(view::showGooglePlayServicesWarningAlert)\n+```\n+\n+Implementation:\n+\n+```kotlin\n+interface GooglePlayServices {\n+\n+    val available: Single<Boolean>\n+\n+    class PackagedGooglePlayServices(\n+        context: AndroidContext,\n+        ioScheduler: Scheduler\n+    ) : GooglePlayServices {\n+\n+        override val available = Single\n+            .fromCallable { GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) }\n+            .map { it == ConnectionResult.SUCCESS }\n+            .subscribeOn(ioScheduler)\n+    }\n+}\n+```\n+\n+The work is done on IO thread since retrieving the information can take an undefined amount of time.\n+Since the RxJava is used all multi-threading mumbo-jumbo is handled automagically.\n+\n+And of course, it is possible to test the behavior without mocking static\n+`GoogleApiAvailability` `getInstance()` method.\n+\n+# Retrospective\n+\n+The beautiful thing about abstractions is that they are universal and can be applied everywhere.","path":"content/posts/2018-08-26-reactive-abstractions.md","position":279,"original_position":279,"commit_id":"c1d11f17a02c06c82427641a462daeaf6fb4254b","original_commit_id":"c1d11f17a02c06c82427641a462daeaf6fb4254b","user":{"login":"artem-zinnatullin","id":967132,"node_id":"MDQ6VXNlcjk2NzEzMg==","avatar_url":"https://avatars0.githubusercontent.com/u/967132?v=4","gravatar_id":"","url":"https://api.github.com/users/artem-zinnatullin","html_url":"https://github.com/artem-zinnatullin","followers_url":"https://api.github.com/users/artem-zinnatullin/followers","following_url":"https://api.github.com/users/artem-zinnatullin/following{/other_user}","gists_url":"https://api.github.com/users/artem-zinnatullin/gists{/gist_id}","starred_url":"https://api.github.com/users/artem-zinnatullin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/artem-zinnatullin/subscriptions","organizations_url":"https://api.github.com/users/artem-zinnatullin/orgs","repos_url":"https://api.github.com/users/artem-zinnatullin/repos","events_url":"https://api.github.com/users/artem-zinnatullin/events{/privacy}","received_events_url":"https://api.github.com/users/artem-zinnatullin/received_events","type":"User","site_admin":false},"body":"yet it's unclear if such platform abstractions should be open sourced and shared (just a thought)","created_at":"2018-08-29T21:13:09Z","updated_at":"2018-08-29T21:14:05Z","html_url":"https://github.com/ming13/pages/pull/7#discussion_r213836194","pull_request_url":"https://api.github.com/repos/ming13/pages/pulls/7","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/ming13/pages/pulls/comments/213836194"},"html":{"href":"https://github.com/ming13/pages/pull/7#discussion_r213836194"},"pull_request":{"href":"https://api.github.com/repos/ming13/pages/pulls/7"}}},"pull_request":{"url":"https://api.github.com/repos/ming13/pages/pulls/7","id":211844321,"node_id":"MDExOlB1bGxSZXF1ZXN0MjExODQ0MzIx","html_url":"https://github.com/ming13/pages/pull/7","diff_url":"https://github.com/ming13/pages/pull/7.diff","patch_url":"https://github.com/ming13/pages/pull/7.patch","issue_url":"https://api.github.com/repos/ming13/pages/issues/7","number":7,"state":"open","locked":false,"title":"Add \"Reactive Abstractions in Android World\" post.","user":{"login":"ming13","id":200401,"node_id":"MDQ6VXNlcjIwMDQwMQ==","avatar_url":"https://avatars1.githubusercontent.com/u/200401?v=4","gravatar_id":"","url":"https://api.github.com/users/ming13","html_url":"https://github.com/ming13","followers_url":"https://api.github.com/users/ming13/followers","following_url":"https://api.github.com/users/ming13/following{/other_user}","gists_url":"https://api.github.com/users/ming13/gists{/gist_id}","starred_url":"https://api.github.com/users/ming13/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ming13/subscriptions","organizations_url":"https://api.github.com/users/ming13/orgs","repos_url":"https://api.github.com/users/ming13/repos","events_url":"https://api.github.com/users/ming13/events{/privacy}","received_events_url":"https://api.github.com/users/ming13/received_events","type":"User","site_admin":false},"body":"@artem-zinnatullin, @dmitry-novikov, PTAL.","created_at":"2018-08-29T18:22:19Z","updated_at":"2018-08-29T21:14:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c808a67fbdfab2698debf07d6263f75e0bd68809","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ming13/pages/pulls/7/commits","review_comments_url":"https://api.github.com/repos/ming13/pages/pulls/7/comments","review_comment_url":"https://api.github.com/repos/ming13/pages/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ming13/pages/issues/7/comments","statuses_url":"https://api.github.com/repos/ming13/pages/statuses/c1d11f17a02c06c82427641a462daeaf6fb4254b","head":{"label":"ming13:ad/post/reactive-abstractions","ref":"ad/post/reactive-abstractions","sha":"c1d11f17a02c06c82427641a462daeaf6fb4254b","user":{"login":"ming13","id":200401,"node_id":"MDQ6VXNlcjIwMDQwMQ==","avatar_url":"https://avatars1.githubusercontent.com/u/200401?v=4","gravatar_id":"","url":"https://api.github.com/users/ming13","html_url":"https://github.com/ming13","followers_url":"https://api.github.com/users/ming13/followers","following_url":"https://api.github.com/users/ming13/following{/other_user}","gists_url":"https://api.github.com/users/ming13/gists{/gist_id}","starred_url":"https://api.github.com/users/ming13/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ming13/subscriptions","organizations_url":"https://api.github.com/users/ming13/orgs","repos_url":"https://api.github.com/users/ming13/repos","events_url":"https://api.github.com/users/ming13/events{/privacy}","received_events_url":"https://api.github.com/users/ming13/received_events","type":"User","site_admin":false},"repo":{"id":116567190,"node_id":"MDEwOlJlcG9zaXRvcnkxMTY1NjcxOTA=","name":"pages","full_name":"ming13/pages","owner":{"login":"ming13","id":200401,"node_id":"MDQ6VXNlcjIwMDQwMQ==","avatar_url":"https://avatars1.githubusercontent.com/u/200401?v=4","gravatar_id":"","url":"https://api.github.com/users/ming13","html_url":"https://github.com/ming13","followers_url":"https://api.github.com/users/ming13/followers","following_url":"https://api.github.com/users/ming13/following{/other_user}","gists_url":"https://api.github.com/users/ming13/gists{/gist_id}","starred_url":"https://api.github.com/users/ming13/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ming13/subscriptions","organizations_url":"https://api.github.com/users/ming13/orgs","repos_url":"https://api.github.com/users/ming13/repos","events_url":"https://api.github.com/users/ming13/events{/privacy}","received_events_url":"https://api.github.com/users/ming13/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ming13/pages","description":null,"fork":false,"url":"https://api.github.com/repos/ming13/pages","forks_url":"https://api.github.com/repos/ming13/pages/forks","keys_url":"https://api.github.com/repos/ming13/pages/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ming13/pages/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ming13/pages/teams","hooks_url":"https://api.github.com/repos/ming13/pages/hooks","issue_events_url":"https://api.github.com/repos/ming13/pages/issues/events{/number}","events_url":"https://api.github.com/repos/ming13/pages/events","assignees_url":"https://api.github.com/repos/ming13/pages/assignees{/user}","branches_url":"https://api.github.com/repos/ming13/pages/branches{/branch}","tags_url":"https://api.github.com/repos/ming13/pages/tags","blobs_url":"https://api.github.com/repos/ming13/pages/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ming13/pages/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ming13/pages/git/refs{/sha}","trees_url":"https://api.github.com/repos/ming13/pages/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ming13/pages/statuses/{sha}","languages_url":"https://api.github.com/repos/ming13/pages/languages","stargazers_url":"https://api.github.com/repos/ming13/pages/stargazers","contributors_url":"https://api.github.com/repos/ming13/pages/contributors","subscribers_url":"https://api.github.com/repos/ming13/pages/subscribers","subscription_url":"https://api.github.com/repos/ming13/pages/subscription","commits_url":"https://api.github.com/repos/ming13/pages/commits{/sha}","git_commits_url":"https://api.github.com/repos/ming13/pages/git/commits{/sha}","comments_url":"https://api.github.com/repos/ming13/pages/comments{/number}","issue_comment_url":"https://api.github.com/repos/ming13/pages/issues/comments{/number}","contents_url":"https://api.github.com/repos/ming13/pages/contents/{+path}","compare_url":"https://api.github.com/repos/ming13/pages/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ming13/pages/merges","archive_url":"https://api.github.com/repos/ming13/pages/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ming13/pages/downloads","issues_url":"https://api.github.com/repos/ming13/pages/issues{/number}","pulls_url":"https://api.github.com/repos/ming13/pages/pulls{/number}","milestones_url":"https://api.github.com/repos/ming13/pages/milestones{/number}","notifications_url":"https://api.github.com/repos/ming13/pages/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ming13/pages/labels{/name}","releases_url":"https://api.github.com/repos/ming13/pages/releases{/id}","deployments_url":"https://api.github.com/repos/ming13/pages/deployments","created_at":"2018-01-07T13:21:56Z","updated_at":"2018-07-31T11:51:04Z","pushed_at":"2018-08-29T18:22:19Z","git_url":"git://github.com/ming13/pages.git","ssh_url":"git@github.com:ming13/pages.git","clone_url":"https://github.com/ming13/pages.git","svn_url":"https://github.com/ming13/pages","homepage":null,"size":110,"stargazers_count":3,"watchers_count":3,"language":"HTML","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":1,"license":null,"forks":1,"open_issues":1,"watchers":3,"default_branch":"master"}},"base":{"label":"ming13:master","ref":"master","sha":"8530c64020db905930e642c61a7c9d2413e9a896","user":{"login":"ming13","id":200401,"node_id":"MDQ6VXNlcjIwMDQwMQ==","avatar_url":"https://avatars1.githubusercontent.com/u/200401?v=4","gravatar_id":"","url":"https://api.github.com/users/ming13","html_url":"https://github.com/ming13","followers_url":"https://api.github.com/users/ming13/followers","following_url":"https://api.github.com/users/ming13/following{/other_user}","gists_url":"https://api.github.com/users/ming13/gists{/gist_id}","starred_url":"https://api.github.com/users/ming13/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ming13/subscriptions","organizations_url":"https://api.github.com/users/ming13/orgs","repos_url":"https://api.github.com/users/ming13/repos","events_url":"https://api.github.com/users/ming13/events{/privacy}","received_events_url":"https://api.github.com/users/ming13/received_events","type":"User","site_admin":false},"repo":{"id":116567190,"node_id":"MDEwOlJlcG9zaXRvcnkxMTY1NjcxOTA=","name":"pages","full_name":"ming13/pages","owner":{"login":"ming13","id":200401,"node_id":"MDQ6VXNlcjIwMDQwMQ==","avatar_url":"https://avatars1.githubusercontent.com/u/200401?v=4","gravatar_id":"","url":"https://api.github.com/users/ming13","html_url":"https://github.com/ming13","followers_url":"https://api.github.com/users/ming13/followers","following_url":"https://api.github.com/users/ming13/following{/other_user}","gists_url":"https://api.github.com/users/ming13/gists{/gist_id}","starred_url":"https://api.github.com/users/ming13/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ming13/subscriptions","organizations_url":"https://api.github.com/users/ming13/orgs","repos_url":"https://api.github.com/users/ming13/repos","events_url":"https://api.github.com/users/ming13/events{/privacy}","received_events_url":"https://api.github.com/users/ming13/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ming13/pages","description":null,"fork":false,"url":"https://api.github.com/repos/ming13/pages","forks_url":"https://api.github.com/repos/ming13/pages/forks","keys_url":"https://api.github.com/repos/ming13/pages/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ming13/pages/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ming13/pages/teams","hooks_url":"https://api.github.com/repos/ming13/pages/hooks","issue_events_url":"https://api.github.com/repos/ming13/pages/issues/events{/number}","events_url":"https://api.github.com/repos/ming13/pages/events","assignees_url":"https://api.github.com/repos/ming13/pages/assignees{/user}","branches_url":"https://api.github.com/repos/ming13/pages/branches{/branch}","tags_url":"https://api.github.com/repos/ming13/pages/tags","blobs_url":"https://api.github.com/repos/ming13/pages/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ming13/pages/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ming13/pages/git/refs{/sha}","trees_url":"https://api.github.com/repos/ming13/pages/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ming13/pages/statuses/{sha}","languages_url":"https://api.github.com/repos/ming13/pages/languages","stargazers_url":"https://api.github.com/repos/ming13/pages/stargazers","contributors_url":"https://api.github.com/repos/ming13/pages/contributors","subscribers_url":"https://api.github.com/repos/ming13/pages/subscribers","subscription_url":"https://api.github.com/repos/ming13/pages/subscription","commits_url":"https://api.github.com/repos/ming13/pages/commits{/sha}","git_commits_url":"https://api.github.com/repos/ming13/pages/git/commits{/sha}","comments_url":"https://api.github.com/repos/ming13/pages/comments{/number}","issue_comment_url":"https://api.github.com/repos/ming13/pages/issues/comments{/number}","contents_url":"https://api.github.com/repos/ming13/pages/contents/{+path}","compare_url":"https://api.github.com/repos/ming13/pages/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ming13/pages/merges","archive_url":"https://api.github.com/repos/ming13/pages/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ming13/pages/downloads","issues_url":"https://api.github.com/repos/ming13/pages/issues{/number}","pulls_url":"https://api.github.com/repos/ming13/pages/pulls{/number}","milestones_url":"https://api.github.com/repos/ming13/pages/milestones{/number}","notifications_url":"https://api.github.com/repos/ming13/pages/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ming13/pages/labels{/name}","releases_url":"https://api.github.com/repos/ming13/pages/releases{/id}","deployments_url":"https://api.github.com/repos/ming13/pages/deployments","created_at":"2018-01-07T13:21:56Z","updated_at":"2018-07-31T11:51:04Z","pushed_at":"2018-08-29T18:22:19Z","git_url":"git://github.com/ming13/pages.git","ssh_url":"git@github.com:ming13/pages.git","clone_url":"https://github.com/ming13/pages.git","svn_url":"https://github.com/ming13/pages","homepage":null,"size":110,"stargazers_count":3,"watchers_count":3,"language":"HTML","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":1,"license":null,"forks":1,"open_issues":1,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ming13/pages/pulls/7"},"html":{"href":"https://github.com/ming13/pages/pull/7"},"issue":{"href":"https://api.github.com/repos/ming13/pages/issues/7"},"comments":{"href":"https://api.github.com/repos/ming13/pages/issues/7/comments"},"review_comments":{"href":"https://api.github.com/repos/ming13/pages/pulls/7/comments"},"review_comment":{"href":"https://api.github.com/repos/ming13/pages/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ming13/pages/pulls/7/commits"},"statuses":{"href":"https://api.github.com/repos/ming13/pages/statuses/c1d11f17a02c06c82427641a462daeaf6fb4254b"}},"author_association":"OWNER"}}
{"actor":{"display_login":"artem-zinnatullin"}}